拓扑排序基础
拓扑排序简要介绍及应用场景
拓扑排序:对图中所有节点进行排序,保证每个节点的前置节点都在这个节点之前。
【使用要求】:有向图,无环
拓扑排序的顺序可能不只一种。拓扑排序也可以用来判断图中有没有环存在。
拓扑排序步骤:
- 在图中找到所有入度为0的点 (入度为0说明此节点没有前置节点,那它只能是其他节点的前置节点,所以先提出来放在拓扑排序前面)
- 把所有入度为0的节点在图中删掉,重点是把与其他邻居节点的关联删除。之后继续找到入度为0的节点并删掉关联。
- 直到所有的节点被删除,依次删除的顺序就是正确的拓扑排序结果
- 如果无法删除所有点,说明有向图中有环存在
我们来看一道拓扑排序的模板题
P
r
o
b
l
e
m
1
Problem1
Problem1 课程表(2) LeetCode210
现在你总共有 numCourses
门课需要选,记为 0
到 numCourses - 1
。给你一个数组 prerequisites
,其中 prerequisites[i] = [ai, bi]
,表示在选修课程 ai
前 必须 先选修 bi
。
- 例如,想要学习课程
0
,你需要先完成课程1
,我们用一个匹配来表示:[0,1]
。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
示例 3:
输入:numCourses = 1, prerequisites = []
输出:[0]
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
- 所有
[ai, bi]
互不相同
问题分析:
这道题是拓扑排序的模板题,[ai,bi]
可以转化为图中的bi
节点指向ai
节点,最终的拓扑排序结果就是题目要求的课程排序。
直接看代码:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> Graph(numCourses);
int indegree[numCourses];
memset(indegree, 0, sizeof(indegree));
// 建图
for (vector<int> vec : prerequisites) {
Graph[vec[1]].push_back(vec[0]);
indegree[vec[0]]++;
}
//用数组模拟队列
vector<int> queue(numCourses);
int left = 0;
int right = 0;
for(int i = 0;i < numCourses;i++){
if(indegree[i] == 0){
queue[right++] = i;
}
}
int cnt = 0;
while(left < right){
int deleteNode = queue[left]; //被删除节点
cnt++;
//现在剔除被删除节点与邻居节点的关联,直接将邻居节点的入度-1
for(int neighbor : Graph[deleteNode]){
indegree[neighbor]--;
if(indegree[neighbor] == 0){
queue[right++] = neighbor;
}
}
left++;
}
return cnt == numCourses ? queue: vector<int>();
}
代码分析:
在解决代码中,我们用了动态结构邻接表去存储图,值得一提的是,我们利用了int indgree[]
作为入度表记录每个节点的入度,先遍历一遍indgree[]
获取一开始入度为0的节点,将其存到队列queue
中,直接用语言本身的queue
比较耗时间,所以我们利用了数组去模拟队列,vector<int> queue(numCourses)
,发现入度为0节点,则在尾部加入此节点,同时队列尾部right++。再取出入度为0节点,同时删除其与邻居节点的关联,删除过程中如果发现邻居节点的入度减为0,则将邻居节点加入到队列中。删除节点完毕之后,left++继续剔除入度为0的节点。
最后我们用cnt
记录删除了多少个节点,如果删除节点个数小于总节点个数,说明图中存在环。
我们再利用链式前向星建图的方式看一道相差无几的拓扑排序模板题
P
r
o
b
l
e
m
2
Problem2
Problem2 拓扑排序模板 牛客AB13
描述
给定一个包含n个点m条边的有向无环图,求出该图的拓扑序。若图的拓扑序不唯一,输出任意合法的拓扑序即可。若该图不能拓扑排序,输出−1。
输入描述:
第一行输入两个整数n,m ( 1≤𝑛,𝑚≤2⋅1051≤n,m≤2⋅105),表示点的个数和边的条数。
接下来的m行,每行输入两个整数
u
i
,
v
i
u_i,v_i
ui,vi(1≤u,v≤n),表示
u
i
u_i
ui到
v
i
v_i
vi之间有一条有向边。
输出描述:
若图存在拓扑序,输出一行n个整数,表示拓扑序。否则输出−1。
示例:
输入:
5 4
1 2
2 3
3 4
4 5
输出:
1 2 3 4 5
原理一模一样,值得一提的只有链式前向星找邻居节点,请看以下代码:
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
int head[200001];
int nextEdge[200001];
int toEdge[200001];
int cnt = 0;
void build() {
cnt = 0;
memset(head, 0, sizeof(head));
memset(nextEdge, 0, sizeof(nextEdge));
memset(toEdge, 0, sizeof(toEdge));
}
void addEdge(int u, int v) {
cnt++;
nextEdge[cnt] = head[u];
toEdge[cnt] = v;
head[u] = cnt;
}
void directGraph(vector<vector<int>>& Edges, int M) {
for (int i = 0; i < M; i++) {
addEdge(Edges[i][0], Edges[i][1]);
}
}
int main() {
int N, M;
scanf("%d", &N);
scanf("%d", &M);
int indgree[N+1]; //入度表
memset(indgree, 0, sizeof(indgree));
vector<vector<int>> Edges(M, vector<int>(2));
for (int i = 0; i < M; i++) {
int from, to;
scanf("%d", &from);
scanf("%d", &to);
Edges[i][0] = from;
Edges[i][1] = to;
indgree[to]++;
}
build();
directGraph(Edges, M);
vector<int> queue(N+1);
int left = 1;
int right = 1;
for (int i = 1; i <= N; i++) {
if (indgree[i] == 0) {
queue[right++] = i;
}
}
int count = 0;
while (left < right) {
int deleteNode = queue[left];
count++;
//删除关联
for (int ei = head[deleteNode]; ei > 0; ei = nextEdge[ei]) {
int neighbor = toEdge[ei];
indgree[neighbor]--;
if (indgree[neighbor] == 0) {
queue[right++] = neighbor;
}
}
left++;
}
if (count != N){
printf("%d", -1);
return 0;
}
for (int i = 1; i <= N - 1; i++) {
printf("%d ", queue[i]);
}
printf("%d",queue[N]);
return 0;
}
这两道题目放一起看,还是邻接表用起来更舒服,写的快一些,不过遇到对空间严苛的题目,要用链式前向星建图法。
第三题会用到 小根堆 这个数据结构,同学们可以去【堆】专栏复习一下堆的知识
P
r
o
b
l
e
m
3
Problem3
Problem3 字典序的拓扑排序 洛谷U107394
题目描述
有向无环图上有n个点,m条边。求这张图字典序最小的拓扑排序的结果。字典序最小指希望排好序的结果中,比较靠前的数字尽可能小。
输入格式
第一行是用空格隔开的两个整数n和m,表示n个点和m条边。
接下来是m行,每行用空格隔开的两个数u和v,表示有一条从u到v的边。
输出格式
输出一行,拓扑排序的结果,数字之间用空格隔开
输入:
5 3
1 2
2 4
4 3
输出:
1 2 4 3 5
问题分析:
我们先重点看一下拓扑排序和字典序最小的概念:
- 拓扑排序:在拓扑排序之后的序列中,每个节点的前置节点都在这个节点之前。
- 字典序最小:序列中比较靠前的数字尽可能小,如果序列按照升序排序,那么它的字典序是最小的。
第一种思路:
我们第一时间肯定是想着在拓扑排序的基础上再做一些操作,使字典序最小,还记得我们之前是怎么在草稿纸中上演算拓扑排序的吗?第一轮,我们把入度为0的数据挑出来,并删除它们在图中的关联;第二轮,把新的入度为0的数据挑出来,并删除它们在图中的关联;这样一轮一轮下去,最终得到完整的拓扑排序结果。
一开始我是这么想的,我用vector<int>
去存储每一轮的结果,并把每一轮的结果按照升序排序,排序之后放到结果数组result
中,这样一轮一轮下去,最终得到一个完整序列,具体操作请看下图:
这个思路经验证之后,我们会发现这是不正确的,10和4没有先后关系,按照字典序最小的规则,4应该在10前面。为什么会存在这种错误呢?是因为我们是一轮一轮来排序的,10是第二轮中的数,4是第三轮的数,即使10和4没有前置关系,但是因为10比4轮次靠前,10就比4先出现。
第二种正确思路:
回顾一下我们之前对拓扑排序的代码实现,我们是用队列来存储各入度为0的节点的,弹出一个入度为0的节点之后将由删除此节点而新产生的入度为0的节点加入到队列。注意,【我们是把前置节点先弹出队列(也就是先访问),之后再加入后续节点的】,只要保证这种操作方式我们得到的序列一定是拓扑排序序列。
同时要保证字典序最小,贪心地想,弹出来的数越小越好嘛,最好是此时图中所有节点中数最小的点,我们借助【小根堆】就能得到最小的点。
到现在解决方式已经很明了了,我们用小根堆代替队列来存储各入度为0的节点,同时保持【将前置节点弹出小根堆,再加入后续节点】操作。具体操作我也给出细节图:
解决代码:
#include <iostream>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;
int head[101];
int nextEdge[1001];
int toEdge[1001];
int cnt = 0;
void build(){
cnt = 0;
memset(head,0,sizeof(head));
memset(nextEdge,0,sizeof(nextEdge));
memset(toEdge,0,sizeof(toEdge));
}
void addEdge(int u,int v){
cnt++;
nextEdge[cnt] = head[u];
toEdge[cnt] = v;
head[u] = cnt;
}
int main(){
build();
int N;
scanf("%d",&N);
vector<int> indgree(N+1, 0);
for(int i = 1;i <= N;i++){
int to;
scanf("%d",&to);
while(to != 0){
addEdge(i,to);
indgree[to]++;
scanf("%d",&to);
}
}
priority_queue<int,vector<int>,greater<int>> pq;
//把初始入度为0的节点加入小根堆中
for(int i = 1; i <= N;i ++){
if(indgree[i] == 0){
pq.push(i);
}
}
vector<int> result;
while(pq.size() != 0){
int deleteNode = pq.top();
result.push_back(deleteNode);
pq.pop();
for(int ei = head[deleteNode];ei > 0; ei = nextEdge[ei]){
indgree[toEdge[ei]]--;
if(indgree[toEdge[ei]] == 0){
pq.push(toEdge[ei]);
}
}
}
for(int i = 0 ;i < N-1;i++){
cout << result[i] << " ";
}
cout << result[N-1];
return 0;
}