拓扑排序C++
几个基本概念的介绍
入度和出度
图中的度:所谓顶点的度(degree),就是指和该顶点相关联的边数。在有向图中,度又分为入度和出度。
入度 (in-degree) :以某顶点为弧头,终止于该顶点的边的数目称为该顶点的入度。
出度 (out-degree) :以某顶点为弧尾,起始于该顶点的弧的数目称为该顶点的出度。
邻接表
邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
-
在有向图中,描述每个点向别的节点连的边(点a->点b这种情况)。
-
在无向图中,描述每个点所有的边(点a-点b这种情况)
LeetCode习题
207. 课程表
解题思路
本题可约化为: 课程安排图是否是有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
思路是通过 拓扑排序 判断此课程安排图是否是有向无环图(DAG) 。
拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 ( u , v ) (u,v) (u,v),均有 u u u(在排序记录中)比 v v v 先出现。亦可理解为对某点 v v v 而言,只有当 v v v 的所有源点均出现了, v v v 才能出现。
通过课程前置条件列表 prerequisites 可以得到课程安排图的邻接表 adjacency,以降低算法时间复杂度,以下两种方法都会用到邻接表。
方法一:入度表(BFS)
本方法中几个数据结构的含义:
-
vector<vector<int>> prerequisites
题目给出参数,其中每个元素p
是一个依赖关系p[0]
依赖于p[1]
,在有向图中,应该是p[1]->p[0]
。 -
vector<int> degress
记录所有节点的入度 -
vector<vector<int>> adjacents
邻接表,长度为总课程数,下标 i i i 的元素存放所有依赖节点 i i i 的节点 -
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); // 记录所有顶点的入度,未初始化的为0
vector<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/