有向图的拓扑序
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
否则输出 −1。数据范围
1≤n,m≤105
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3 -
题目来源:https://www.acwing.com/problem/content/850/
题目分析:
- 有向图,存在自环/重边,则可能根本没有拓扑序,需要鉴别
- 稀疏图,采用静态邻接表
- 拓扑序列:可能含有多种,只需输出其中一种
- 下面讲解队列拓扑序,分析为什么拓扑序列可能含有多种,又可能没有
算法原理:
模板算法:
有向图拓扑序:
1. 概念和要求:
-
入度:有多少边进入该点
出度:有多少边出了该点
-
范围:有向图
-
要求:无环,无自环 & 无他环
2. 排序过程:
-
选择图中入度为0的点进入拓扑序序列
-
点入序列后,视作该点产生的边消失
则产生新的入度为0的点
-
重复入序列,消边的过程,直到有向无环图中所有点都在拓扑序列中
- 上图拓扑序:1 -> 2 -> 3
3. 数据结构:
-
为每个点建立入度数组,不需要出度数组
-
入度为0的点进入队列
遍历从该点可以到达的点,其入度减一
若入度为0,加入序列 -
队列为空时,可能所有点入过队
也可能有的点如环中点和非连通点没有入队
此时拓扑序在q[]队列的0 ~ tt中
4.拓扑序的意义:
-
拓扑序左边的点可以到达右边,说明左边的点出度很大,右边的点入度很大
-
拓扑序右边的点不可以到达左边,说明右边的点出度很小,左边的点入读很小
代码实现:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n = 0, m = 0;
int h[N], val[2*N], ne[2*N], idx;
void insert(int x, int y){
val[idx] = y;
ne[idx] = h[x];
h[x] = idx++;
}
int que[N], tt, hh;
int rudu[N];
int res[N], k;
bool topsort(){
for(int i=1; i<=n; i++)
if (!rudu[i]){
que[tt++] = i;
res[k++] = i;
}
while(hh <= tt){
int x = que[hh++];
for(int i = h[x]; i!=-1; i=ne[i]){
rudu[val[i]]--;
if (!rudu[val[i]]){
que[tt++] = val[i];
res[k++] = val[i];
}
}
}
if(k==n) return 1;
}
int main(){
memset(h, -1, sizeof(h));
cin >>n >>m;
while(m--){
int x, y;
cin >>x >>y;
rudu[y]++;
insert(x, y);
}
if (!topsort()) cout<<-1;
else {
for(int i=0; i<k; i++){
cout<<res[i];
if (i!=k-1) cout<<" ";
}
}
return 0;
}
代码误区:
1. 为什么是有向图?
- 拓扑序本身针对的是入度进行排序
- 当然自己写一个无向图的度的拓扑序也很容易
2. 边的存储:
- 静态邻接表:
输入x y之后,将x开头的邻接表中加入图论节点y
则以x开头的邻接表都是以x为起点,图论节点为终点
删除x之后,x链表上的所有终点节点入度– - 邻接矩阵:
遍历arr[x][j]这一行,所有以x为起点y为终点的边都存储在arr[x][j]
反之,arr[j][x]是x为终点,j为起点的边
3. 为什么有向图的拓扑序有多种?
-
删除一个点后造成的入度为0的点很多
-
代码中依照邻接表顺序将入度为0的点加入到队列和结果数组中
当然也可以将邻接表中入度为0的点排个序后插入到队列和结果数组中
甚至将邻接表中入度为0的点乱序之后插入到队列和结果数组中
-
注意我这个描述,先排序,再入队列和结果数组
如果你先入队列或结果数组后再排序,本身就有悖于拓扑序。
4. 为什么有的有向无环图无拓扑序
-
自环:始终存在着一个入度,要想这个入度化0,需要先让该点加入队列;要加入队列,需要入度为0。
-
他环:本身就没有入度为0的点,队列一直为空
-
非连通:其实是两幅独立的图,对一幅加工之后,对另一幅0影响
本篇感想:
- OK了,截止本篇结束,所有图论入门完结,下面进入最短路径5大算法以及最小生成树2大算法。
- 看完本篇博客,恭喜已登 《筑基境-初期》
距离登仙境不远了,加油