拓扑排序C++
几个基本概念的介绍
入度和出度
图中的度:所谓顶点的度(degree),就是指和该顶点相关联的边数。在有向图中,度又分为入度和出度。
入度 (in-degree) :以某顶点为弧头,终止于该顶点的边的数目称为该顶点的入度。
出度 (out-degree) :以某顶点为弧尾,起始于该顶点的弧的数目称为该顶点的出度。
邻接表
邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
-
在有向图中,描述每个点向别的节点连的边(点a->点b这种情况)。
-
在无向图中,描述每个点所有的边(点a-点b这种情况)
LeetCode习题
207. 课程表
解题思路
本题可约化为: 课程安排图是否是有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
思路是通过 拓扑排序 判断此课程安排图是否是有向无环图(DAG) 。
拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 (u,v)(u,v)(u,v),均有 uuu(在排序记录中)比 vvv 先出现。亦可理解为对某点 vvv 而言,只有当 vvv 的所有源点均出现了,vvv 才能出现。
通过课程前置条件列表 prerequisites 可以得到课程安排图的邻接表 adjacency,以降低算法时间复杂度,以下两种方法都会用到邻接表。
方法一:入度表(BFS)
本方法中几个数据结构的含义:
-
vector<vector<int>> prerequisites
题目给出参数,其中每个元素p
是一个依赖关系p[0]
依赖于p[1]
,在有向图中,应该是p[1]->p[0]
。 -
vector<int> degress
记录所有节点的入度 -
vector<vector<int>> adjacents
邻接表,长度为总课程数,下标 iii 的元素存放所有依赖节点 iii 的节点 -
queue<int> zeros
存放所有目前入度为 0 的顶点
算法流程:
- 统计课程安排图中每个节点的入度,生成 入度表
indegrees
。 - 借助一个队列
queue
,将所有入度为 0 (没有任何依赖)的节点入队。 - 当
queue
非空时,依次将队首节点出队,在课程安排图中删除此节点pre
:- 并不是真正从邻接表中删除此节点
pre
,而是将此节点邻接表对应所有邻接节点cur
,即所有以来该节点的节点的入度 −1,即indegrees[cur] -= 1
。 - 当入度 −1 后邻接节点
cur
的入度为 0,说明cur
所有的前驱节点(依赖节点)已经被 “删除”,此时将cur
入队。
- 并不是真正从邻接表中删除此节点
- 在每次
pre
出队时,执行numCourses--
;- 若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
- 因此,拓扑排序出队次数等于课程个数,返回
numCourses == 0
判断课程是否可以成功安排。
class Solution {
public:bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {vector<int> degrees(numCourses, 0); // 记录所有顶点的入度,未初始化的为0vector<vector<int>> adjacents(numCourses); // 邻接表queue<int> zero; // 零入度的顶点int num = numCourses;for (int i=0; i<prerequisites.size(); ++i) {degrees[prerequisites[i][0]]++; // 入顶点adjacents[prerequisites[i][1]].push_back(prerequisites[i][0]); // 出顶点}for (int i=0; i<numCourses; ++i) {if (degrees[i] == 0) {zero.push(i); // 入度为0的先入队列--num;}}while (!zero.empty()) {int temp = zero.front();zero.pop();for (int j=0; j<adjacents[temp].size(); ++j) {if (--degrees[adjacents[temp][j]] == 0) {zero.push(adjacents[temp][j]);--num;}}}if (num == 0) return true;else return false;}
};
方法二:DFS
原理是通过 DFS 判断图中是否有环。
算法流程:
- 借助一个标志列表
flag
,用于判断每个节点i
(课程)的状态:- 未被 DFS 访问:
i == 0
; - 已被其他节点启动的 DFS 访问:
i == -1
; - 已被当前节点启动的 DFS 访问:
i == 1
。
- 未被 DFS 访问:
- 对
numCourses
个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程:- 终止条件:
- 当
flag[i] == -1
,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。 - 当
flag[i] == 1
,说明在本轮 DFS 搜索中节点i
被第 2 次访问,即 课程安排图有环 ,直接返回 False。
- 当
- 将当前访问节点
i
对应flag[i]
置 1,即标记其被本轮 DFS 访问过; - 递归访问当前节点
i
的所有邻接节点j
,当发现环直接返回 False; - 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点
flag
置为 −1 并返回 True。
- 终止条件:
- 若整个图 DFS 结束并未发现环,返回 True。
class Solution {
public:bool dfs(vector<vector<int>>& adjacents, vector<int>& flags, int curr) {if (flags[curr] == 1) return false;else if (flags[curr] == -1) return true;flags[curr] = 1;for (int i=0; i<adjacents[curr].size(); ++i) {if (!dfs(adjacents, flags, adjacents[curr][i])) return false;}flags[curr] = -1;return true;}bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {vector<vector<int>> adjacents(numCourses);vector<int> flags(numCourses, 0);for (vector<int> p: prerequisites) {adjacents[p[1]].push_back(p[0]);}for (int i=0; i<numCourses; ++i) {if (!dfs(adjacents, flags, i)) return false;}return true;}
};
210. 课程表 II
与上题思路一致
BFS
class Solution {
private:// 存储有向图vector<vector<int>> edges;// 存储每个节点的入度vector<int> indeg;// 存储答案vector<int> result;public:vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {edges.resize(numCourses);indeg.resize(numCourses);for (const auto& info: prerequisites) {edges[info[1]].push_back(info[0]);++indeg[info[0]];}queue<int> q;// 将所有入度为 0 的节点放入队列中for (int i = 0; i < numCourses; ++i) {if (indeg[i] == 0) {q.push(i);}}while (!q.empty()) {// 从队首取出一个节点int u = q.front();q.pop();// 放入答案中result.push_back(u);for (int v: edges[u]) {--indeg[v];// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了if (indeg[v] == 0) {q.push(v);}}}if (result.size() != numCourses) {return {};}return result;}
};
DFS
class Solution {
private:vector<vector<int>> edges;vector<int> visited;bool valid = true;stack<int> S;void dfs(int u) {visited[u] = 1;for (int v : edges[u]) {if ( visited[v] == 1) {valid = false;return;}else if (visited[v] == 0) {dfs(v);if (!valid) return;}}visited[u] = 2;S.push(u);}public:vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {edges.resize(numCourses);visited.resize(numCourses);for (const auto& v : prerequisites) {edges[v[1]].push_back(v[0]);}for (int i=0; i<numCourses && valid; i++) {if (!visited[i]) dfs(i);}if (!valid) return {};else {vector<int> res;while (!S.empty()) {res.push_back(S.top());S.pop();}return res;}
解题思路参考:https://leetcode-cn.com/problems/course-schedule/solution/course-schedule-tuo-bu-pai-xu-bfsdfsliang-chong-fa/