1、算法思路
拓扑排序是对有向无环图的顶点的一种排序,它使得如果存在一条从u到v的一条路径的话,那么在排序中,v一定出现在u的后面。最接近生活的一个例子就是我们修的课程。
图片来源
我们一定是先修完一门基础课,再去修进阶课程。
显然如果图中含有圈,那么拓扑排序一定是不可能的。
在实现上我们采用卡恩算法进行实现:
卡恩于1962年提出了该算法。简单来说,假设L是存放结果的列表,先找到那些入度为零的节点,把这些节点放到L中,因为这些节点没有任何的父节点。然后把与这些节点相连的边从图中去掉,再寻找图中的入度为零的节点。对于新找到的这些入度为零的节点来说,他们的父节点已经都在L中了,所以也可以放入L。重复上述操作,直到找不到入度为零的节点。如果此时L中的元素个数和节点总数相同,说明排序完成;如果L中的元素个数和节点总数不同,说明原图中存在环,无法进行拓扑排序。
L ← 包含已排序的元素的列表,目前为空
S ← 入度为零的节点的集合
当 S 非空时:
将节点n从S移走
将n加到L尾部
选出任意起点为n的边e = (n,m),移除e。如m没有其它入边,则将m加入S。
重复上一步。
如图中有剩余的边则:
return error (图中至少有一个环)
否则:
return L (L为图的拓扑排序)
2、例题
1.有向图的拓扑排序
给定一个 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≤10^5
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int e[N],ne[N],idx,h[N];
int q[N],tt, hh;
int d[N];
void ins(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
int main()
{
int n,m;
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0; i < m; i ++)
{
int a,b;
cin >> a >> b;
ins(a,b);
d[b] ++;
}
hh = 0,tt = -1;
for(int i = 1; i <= n; i ++)
{
if(d[i] == 0)
{
q[++ tt] = i;
}
}
while(tt >= hh)
{
int t = q[hh];
hh ++;
for(int i = h[t]; i != -1; i = ne[i])
{
if(-- d[e[i]] == 0)
{
q[++ tt] = e[i];
}
}
if(tt == n - 1)
{
for(int i = 0; i < n; i ++)
{
cout << q[i] << ' ';
}
return 0;
}
}
cout << "-1";
}
实现的思路基本上是按照上面的卡恩算法进行的,对于题目中的重边,这对拓扑排序的结果没有影响,而对于包含自环的图,他也不会有拓扑排序。
参考资料
- https://www.jianshu.com/p/0b92b442dca7
- 数据结构与算法分析C语言描述(第二版) 9.2
- Acwing
- 维基百科 拓扑排序词条