从拓扑排序看有向图的应用
目录
一、拓扑排序的基本思想
二、外星文字典问题(LCR114)
三、课程表问题(207 和 210)
四、总结
在图论中,拓扑排序是一个非常重要的概念,它可以将有向无环图(DAG)中的节点按照一定的顺序排列,使得对于每一条有向边 u -> v ,节点 u 都在节点 v 之前。今天我们就来探讨拓扑排序在外星文字典和课程表这两个问题中的应用。
一、拓扑排序的基本思想
拓扑排序的核心思想是不断寻找入度为 0 的节点(即没有前驱节点的节点),将其加入结果序列,然后删除该节点以及从该节点出发的所有边,并更新相关节点的入度。重复这个过程,直到所有节点都被处理或者图中存在环(此时无法完成拓扑排序)。
二、外星文字典问题(LCR114)
问题描述
给定一组外星语言的单词,这些单词遵循某种未知的字典序,我们需要根据这些单词推断出外星文字母的顺序。
代码解析
class Solution {
public:unordered_map<char, unordered_set<char>> edges;unordered_map<char, int> in;bool check = false;string alienOrder(vector<string>& words) {// 初始化所有出现的字符的入度为 0for (auto& e : words)for (auto s : e)in[s] = 0;int n = words.size();for (int i = 0; i < n; i++) {for (int j = i + 1; j < n; j++) {add(words[i], words],]);// 如果发现矛盾,直接返回空字符串if (check)return "";}}queue<char> q;// 将入度为 0 的字符加入队列for (auto& [a, b] : in)if (b == 0)q.push(a);string ret;while (!q.empty()) {char t = q.front();q.pop();ret += t;// 处理当前字符指向的所有字符for (char ch : edges[t])if (--in[ch] == 0)q.push(ch);}// 检查是否有环for (auto& [a, b] : in)if (b != 0)return "";return ret;}void add(string& s1, string& s2) {int n = min(s1.size(), s2.size());int i = 0;for (; i < n; i++) {if (s1[i] != s2[i]) {char a = s1[i], b = s2[i];// 如果边不存在,添加边并更新入度if (!edges.count(a) || !edges[a].count(b)) {edges[a].insert(b);in[b]++;}break;}}// 处理 s2 是 s1 前缀且 s1 更长的情况,此时矛盾if (i == s2.size() && i < s1.size())check = true;}
};
- 构建有向图:通过比较每一对单词,找到第一个不同的字符,从而确定字符之间的先后顺序,构建有向边。例如,若单词 s1 的第 i 个字符和单词 s2 的第 i 个字符不同,且 s1[i] 在 s2[i] 之前,则添加边 s1[i] -> s2[i] 。
- 处理矛盾情况:如果出现短单词是长单词的前缀且短单词在长单词之后的情况(如 ["abc", "ab"] ),则存在矛盾,无法确定字典序。
- 拓扑排序:使用队列进行拓扑排序,将入度为 0 的字符依次加入结果,直到所有字符处理完毕或检测到环。
三、课程表问题(207 和 210)
问题描述
- 207 题:判断是否可以完成所有课程的学习,即课程之间的先修关系是否存在环。
- 210 题:不仅要判断是否可以完成所有课程,还要返回一个可能的学习顺序(拓扑排序的结果)。
207 题代码解析(判断是否能完成课程)
class Solution {
public:bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {unordered_map<int, vector<int>> edges;vector<int> in(numCourses, 0);// 构建有向图和入度数组for (auto e : prerequisites) {int a = e[0], b = e[1]; // b 是 a 的先修课,即 b -> aedges[b].push_back(a);in[a]++;}queue<int> q;// 将入度为 0 的课程加入队列for (int i = 0; i < in.size(); i++)if (in[i] == 0)q.push(i);while (!q.empty()) {int tmp = q.front();q.pop();// 处理当前课程的后续课程for (int t : edges[tmp]) {in[t]--;if (in[t] == 0)q.push(t);}}// 检查是否有环for (int i = 0; i < in.size(); i++) {if (in[i])return false;}return true;}
};
- 构建课程之间的有向图, prerequisites[i] = [a, b] 表示 b 是 a 的先修课,即有向边 b -> a 。
- 统计每个课程的入度(有多少门先修课)。
- 利用拓扑排序,处理入度为 0 的课程,最后检查是否所有课程的入度都为 0 (无环)。
210 题代码解析(返回课程学习顺序)
class Solution {
public:vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {unordered_map<int, vector<int>> g;vector<int> in(numCourses, 0);vector<int> ret;// 构建有向图和入度数组for (auto e : prerequisites) {int a = e[0], b = e[1]; // b -> ag[b].push_back(a);in[a]++;}queue<int> q;// 将入度为 0 的课程加入队列,并加入结果for (int i = 0; i < in.size(); i++)if (in[i] == 0) {q.push(i);ret.push_back(i);}while (!q.empty()) {int tmp = q.front();q.pop();// 处理后续课程for (int a : g[tmp]) {in[a]--;if (in[a] == 0) {q.push(a);ret.push_back(a);}}}// 检查是否有环,有环则返回空数组for (int i = 0; i < in.size(); i++)if (in[i]) return {};return ret;}
};
与 207 题类似,只是在拓扑排序过程中,将处理的课程依次加入结果数组 ret ,最后若无环则返回 ret ,否则返回空数组。
四、总结
拓扑排序在处理有向无环图的顺序问题上非常有效,无论是推断外星文字母的顺序,还是确定课程的学习顺序,都能通过构建有向图,然后利用拓扑排序来解决。其核心都是找到入度为 0 的节点,逐步处理,最终判断是否存在环以及得到有效的顺序。