目录
一、何为拓扑排序
1.1 有向无环图(DAG)
一张图若为有向无环图(DAG, Directed Acyclic Graph),则必须满足:
- 该图为有向图,且每个顶点只出现一次。
- 图中没有环的存在
一张DAG图可以有很多种表示方式,如链表表示(用指针存储后继),数组表示(用数组表示后继) 等。故DAG题目多变,但解法与思想都是一样的。
1.2 如何判断DAG
对于图的遍历过程而言, 若在遍历A元素的所有后继(包括后继的后继...)的时候,再次遍历到了A,说明图中存在环,非DAG。
对于删除入度为0的元素的过程而言,若某一时刻不存在入度为0的元素,但依然图中有元素存在,说明图中存在环,非DAG。(“入度”:指向该结点的边的条数)
1.3 拓扑排序
拓扑排序就是遍历一张DAG图,得出遍历元素的先后顺序的过程。若把图中所有顶点视为“事件”,把有向边视为“先后顺序”,则可以将DAG图视为一个表示各事件发生先后顺序的“AOV 网” 。而对DAG进行拓扑排序,就是要找出一个合理的做事情的顺序。与拓扑排序有关的题,基本都是这个类型。
二、拓扑排序的方法
2.1 遍历冲突法
遍历冲突法的基本思想是:若在遍历A元素的所有后继(包括后继的后继...)的时候,再次遍历到了A,说明图中存在环,非DAG。
具体怎么实现呢?我们可以设置一个 visited 数组,用来表示每个结点的搜索状态,搜索状态有三种——0:未搜索、1:正在搜索其全部后继、2:搜索完成。采用深度优先的递归遍历的方式:
- 将visited初始值全部设为0
- 准备搜索A,检查visited[A]:
- 若visited[A] == 1,说明A已处于搜索状态:A发起的搜索重新搜索到了A,图中存在环,非DAG图,结束拓扑排序。
- 若visited[A] == 2,说明A及其全部后继已被搜索,不再做任何处理。
- 若visited[A] == 0, 将visited[A]置为1,并递归搜索A的全部后继。
- 若在递归搜索的某时刻,发现visited 为 1,说明出现了环,结束拓扑排序
- 若所有元素都成功完成搜索,说明这是一张DAG,拓扑排序的结果就是搜索元素的顺序
例题: 见第三节——3.1 课程表
2.2 删除入度为0结点法
删除入度为0结点法的思路是:每次遍历都删除入度为0的节点,若最后图中节点被全部删除,则说明此位DAG图;若最后图中依然有节点,那么这些节点的入度都不为0,则说明出现了无法打破的环,非DAG图。
具体的实现方式是,维护三个数组:
- vector<vector<int>> successors; // 直接后继
- vector<int> status; // 搜索状态
- vector<int> degree; // 入度
每一次都迭代,都选取入度为0,且搜索状态为未搜索的节点遍历:
- 将此节点标记为已搜索
- 将此节点所有直接后继的入度 -1
- 重新开始迭代
* 之所以要重新开始迭代,是因为此次搜索可能改变了之前节点的入度,迭代需要从0重新开始
迭代完成后,检查degree数组,若依然存在不为0的值,说明非DAG图
例题:见第三节——3.2 课程表2
三、例题详解
3.1 课程表
题目:
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。(题目来源:LeetCode https://leetcode.cn/problems/course-schedule/)
思路:
遍历冲突法。
采用DFS进行遍历,用一个栈来存储所有已经搜索完成的节点。
对于一个节点 u,当它的所有直接后继都已经搜索完成(入栈),那么 u 便搜索完成。
假设我们当前搜索到了节点 u,如果它的所有相邻节点都已经搜索完成,那么这些节点都已经在栈中了,此时我们把 u 入栈。此时,我们从栈顶向下看,由于 u 处于栈顶的位置,u 的所有直接后继都在 u 的下方,且每一次更新栈 都满足这个规律。说明栈就是拓扑排序的结果。
算法:
对于图中的任意一个节点,它在搜索的过程中有三种状态,即:
- 「未搜索」:我们还没有搜索到这个节点;
- 「搜索中」:我们搜索过这个节点,但其后继还没有搜索完成(没有入栈),该元素也没有入栈
- 「已完成」:该元素的所有后继都已搜索完成(入栈),该元素也已入栈。
在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。
我们将当前搜索的节点 u 标记为「搜索中」,遍历该节点的每一个相邻节点 v:
- 如果 v 为「未搜索」,那么我们开始搜索 v,待搜索完成回溯到 u;
- 如果 v 为「搜索中」,那么我们就找到了图中的一个环,结束拓扑排序;
- 如果 v 为「已完成」,直接返回。
当 u 的所有相邻节点都为「已完成」时,我们将 uu 放入栈中,并将其标记为「已完成」。
在整个深度优先搜索的过程结束后,如果我们没有找到图中的环,那么栈中存储这所有的 nn 个节点,从栈顶到栈底的顺序即为一种拓扑排序。
作者:LeetCode-Solution
链接:力扣
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> edges; // edges[i]存储元素i的直接后继
vector<int> visited; // 0未搜索,1搜索中,2搜索完成
bool valid = true; // DAG判断标志
void dfs(int u) {
// 搜索开始,将visited置1
visited[u] = 1;
// 递归搜索u的所有后继
for (int v: edges[u]) {
if (visited[v] == 0) {
dfs(v);
if (!valid)
return;
}
// 出现了环
else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
edges.resize(numCourses);
visited.resize(numCourses, 0);
// 将 1->2, 1->3 整合为 edges[1] = {2,3}
for (const auto& info: prerequisites)
edges[info[1]].push_back(info[0]);
// dfs
for (int i = 0; i < numCourses && valid; ++i)
if (!visited[i])
dfs(i);
return valid;
}
int main() {
int numCourses = 10;
vector<vector<int>> prerequisites = {{5,8},{8,5},{1,9},{4,5},{0,2},{7,8},{4,9}};
bool flag = canFinish(numCourses, prerequisites);
if (flag)
cout << "Topologic sort success, there are no rings." ;
else
cout << "Topologic sort fail, there are rings." ;
return 0;
}
3.2 课程表2
题目:
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。(题目来源:https://leetcode.cn/problems/course-schedule-ii/)
思路:删除入度为0节点法。每一次迭代都“删除”一个入度为0的点,看图中最后是否有余留。
算法:
维护successor、status、degree三个数组。每一次迭代都只搜索入度为0且未搜索过的节点,搜索工作如下:
- 将此节点加入res
- 将此节点标记为已搜索
- 将此节点的所有后继的入度 -1
- 将指针置为 -1(此次跌打结束会有i++,i将变为0),重新开始迭代
在迭代完成后,需要判断是否为DAG图,只需要计较res.size()和numCourses即可。
这种方法的效率不如遍历法
// 必须每次都选取入度为0的节点搜索
// 不再使用status = 1来判断,而是用入度来判断
class Solution {
private:
vector<vector<int>> successors; // 直接后继
vector<int> status; // 搜索状态
vector<int> degree; // 入度
vector<int> res;
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
// successors
successors.resize(numCourses);
for (auto& prerequisite : prerequisites)
successors[prerequisite[1]].emplace_back(prerequisite[0]);
// status
status.resize(numCourses, 0);
// degree
degree.resize(numCourses, 0);
for (auto& prerequisite : prerequisites)
degree[prerequisite[0]]++;
// 搜索入度为0且未搜索过的点
for (int i = 0; i < numCourses; i++) {
if (degree[i] == 0 && status[i] == 0) {
res.emplace_back(i); // 加入res
status[i] = 2; // 标记已搜索
for (auto& successor : successors[i]) // 其后继的入度-1
degree[successor]--;
i = -1; // 重新开始搜索
}
}
// 先判断是否所有点都在res中,再返回
if (res.size() == numCourses)
return res;
else
return {};
}
};