拓扑排序¶
约 1019 个字 157 行代码 1 张图片 预计阅读时间 7 分钟
拓扑排序¶
给定一个 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≤1e5
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
思路:
AC代码
C++
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
//邻接表表示方法
int h[N],e[N*2],ne[N*2],idx;
int d[N*2];
int q[N*2];//定义一个队列
int n,m;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
bool topsort()
{
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
if(!d[i]) q[++tt]=i;//将入度为0的点入队
while(hh<=tt)
{
auto t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
d[j]--;
if(d[j]==0) q[++tt]=j;
}
}
return tt==n-1;//如果队列里面有n个点,则存在拓扑序列,否则有环,不存在拓扑序列
}
int main()
{
memset(h,-1,sizeof h);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
d[b]++;//b的入度++
}
if(topsort())
{
for(int i=0;i<n;i++)
cout<<q[i]<<" ";
puts("");
}
else puts("-1");
return 0;
}
拓扑排序stl实现¶
C++
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
const int N=1e6+10;
// 邻接表存储图
int h[N], e[N*2], ne[N*2], idx; // h: 头节点,e: 目标点,ne: 下一条边,idx: 记录边序号
int d[N*2]; // 记录入度
queue<int> q;
int n, m; // 节点数,边数
// 添加边 a -> b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++; // 建立邻接表
}
// 拓扑排序(Kahn 算法)
bool topsort()
{
for(int i = 1; i <= n; i++)
if(!d[i]) q.push(i); // 先将入度为 0 的点入队
int cnt = 0; // 统计拓扑序列中的节点数
while(!q.empty())
{
int t = q.front(); q.pop(); // 取出队头
for(int i = h[t]; i != -1; i = ne[i]) // 遍历 t 的所有出边
{
int j = e[i]; // 目标节点
d[j]--; // 入度减少
if(d[j] == 0) q.push(j); // 入度变 0,入队
}
cnt++; // 统计遍历的点数
}
return cnt == n; // 如果遍历所有点,说明无环,否则有环
}
int main()
{
memset(h, -1, sizeof h); // 初始化邻接表(所有点无边)
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b); // 添加有向边 a -> b
d[b]++; // b 的入度 +1
}
if(topsort()) puts("1"); // 无环
else puts("-1"); // 有环
return 0;
}
详解:¶
拓扑排序的概念¶
拓扑排序(Topological Sorting)是一种用于**有向无环图(DAG,Directed Acyclic Graph)**的排序方法。它将图中的所有顶点排成一个线性序列,使得对于图中的每一条有向边 (u, v)
,u
都排在 v
之前。
拓扑排序的用途¶
拓扑排序常用于:
-
依赖关系处理
- 任务调度(如课程安排,先学基础课再学进阶课)。
- 编译依赖(如文件编译顺序)。
-
解决数据流或事件流中的因果关系问题。
2. 检测有向图是否有环 -
如果一个图的所有点都能拓扑排序,则说明图是DAG(无环图) 。
- 否则,说明图中存在环,无法进行拓扑排序。
代码解析¶
你的代码使用邻接表存储图,并通过入度统计+队列的方法进行拓扑排序。
1. 邻接表的存储¶
-
h[i]
:表示顶点 i
的邻接表头结点(初始值设为-1
)。 -
e[idx]
:存储边的终点。 -
ne[idx]
:存储下一个邻接点的索引,形成链表。 -
idx
:维护当前边的编号。
2. 添加有向边¶
这部分代码实现邻接表的构建:
-
e[idx] = b
:存储边a → b
终点b
。 -
ne[idx] = h[a]
:将当前边加入a
的邻接表。 -
h[a] = idx++
:更新h[a]
使其指向最新加入的边。
3. 拓扑排序的实现¶
-
d[i]
记录顶点 i
的入度(即有多少条边指向i
)。 - 遍历
1 ~ n
,将所有入度为 0
的点入队列 q
。
C++
while(hh<=tt)
{
auto t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
d[j]--;
if(d[j]==0) q[++tt]=j; // 入度变为0则入队
}
}
- 从队列取出一个点
t
,表示已将 t
放入拓扑序列。 - 遍历
t
的所有邻接点j
,入度 d[j]--
。 - 若
d[j] == 0
,说明 j
没有前置依赖,加入队列。
- 如果所有
n
个点都被加入队列,则存在拓扑序列,返回true
。 - 如果有环,则
tt < n-1
,返回false
。
4. 代码主函数¶
C++
int main()
{
memset(h,-1,sizeof h);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
d[b]++;// b 的入度 ++
}
- 先将
h
数组初始化为-1
,表示所有点的邻接表为空。 - 读取
m
条有向边,并建立邻接表,同时更新b
的入度。
- 如果拓扑排序成功,输出
q
(即拓扑序列)。 - 否则输出
-1
,表示图中有环,无法拓扑排序。
运行示例¶
示例1:无环¶
输入¶
图的结构¶
输出¶
说明可以按 1 → 3 → 2 → 4 → 5
这样的顺序完成拓扑排序。
示例2:有环¶
输入¶
图的结构¶
输出¶
因为 1 → 2 → 3 → 1
形成了环,所以无法进行拓扑排序。
总结¶
- 拓扑排序的时间复杂度:
O(n + m)
,适用于大规模 DAG 的排序问题。 - 队列法(Kahn算法) :使用入度统计 + BFS,适用于判断环、任务调度、依赖管理等问题。
- 代码逻辑:
- 初始化入度,找出所有
d[i] = 0
的点入队。 - 循环出队,依次减少相邻点的入度,若
d[j] = 0
再入队。 - 若能遍历完
n
个点,说明无环;否则有环。