拓扑排序学习笔记
前言
在学习这篇文章之前,你需要了解的算法有:
- 基本图论知识
- 链式前向星(图的一种存储方式)
- 了解 队列、栈 等简单数据结构的实现,用
STL
也行。
什么是拓扑排序
AOV网的定义
在了解拓扑排序(topo sort)之前,我们还得了解一个东西:AOV 网。
AOV 网(Activity On Vertex) 主要用于表示活动间的优先关系。
看下面一个 非常不 生动的例子。
假设你在玩游戏,你想要解锁一个宝箱。
想要解锁这个宝箱,你需要先完成一堆任务,而每个任务又需要先完成子任务后才能解锁。
具体如下:
那么 AOV 网长啥样呢?
先观察这个 AOV 网,自己寻找规律。
现在,请思考一下 AOV 网是如何创建的。
(思考ing)
思考完了吧,其实规律是很好找的,就一句。
- 如果
,说明完成任务 之前要先完成任务 。
比如,想完成
看一下图,
进一步讲:
- 如果存在一条路径使得
通向 ,则完成任务 之前要先完成任务 。
不过这一条件并没有太多作用。
以上就是 AOV 网大致的规律了,需要注意的是:
- 如果图上存在环,必定没有办法完成全部任务。
- 即,AOV 网必为一个 DAG(Directed Acycline Graph,有向无环图)。
拓扑排序的定义
说了这么一大坨,终于可以开始讲拓扑排序是神马东西了。
其实这东西特别简单,就是求一个序列,满足:
- 按照序列的顺序完成任务,可以将全部任务完成。
需要注意的是:
- 拓扑序列应该为
到 的其中一个全排列。 - 如果图上存在环,将没有拓扑序列(这一点前文提到过)。
- 拓扑序列一般有多个,但是程序通常只能求其中一个。
而拓扑排序,就是求出这个拓扑序列的。
下面,我们正式讲解拓扑排序的实现。
思路实现
1
用链式前向星存储这个图,并顺手计算每个点的入度。
不作解释,很简单。
int head[N], cur;
struct Node
{
int now, nxt;
}e[M];
void add(int x, int y)
{
e[++cur].now = y;
e[cur].nxt = head[x];
head[x] = cur;
}
int in[N]; //入度
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int x, y;
scanf("%d%d", &x, &y);
add(x, y); //x->y
in[y]++;
}
2
准备一个队列,并将所有入度为
代码:
int cur = 0;
queue <int> Q;
for (int i = 1; i <= n; i++)
if (in[i] == 0)
Q.push(i);
3
每次都将队首计入 topo[]
数组中。
然后遍历所有队首点可以一次到达的点,如
(正是因为这一点,我们采用链式前向星)
然后将遍历到的点 in[x]--
。
若当前点入度被减成
一直执行,直到队列为空。
代码:
int topo[N];
while (!Q.empty())
{
int x = Q.front(); //截取队首
Q.pop(); //弹出
topo[++cur] = x; //扔进拓扑序列中
for (int i = head[x]; i; i = e[i].nxt)
{
int t = e[i].now; //易错,是 e[i].now 而非 i
in[t]--;
if (in[t] == 0) Q.push(t);
}
}
4
最后,我们要判断是否存在拓扑序列。
很简单,不存在拓扑序列,当且仅当
为什么呢?
看这张图,显然是一个普通的环。
所有点入度都不为
那如果外面还有一些点呢?
此处的点
假设 点
你会发现,点
综上所述,可以轻松判断拓扑序列是否符合要求。
代码:
if (cur < n) printf("This graph isn't a DAG."); //图存在环
else
{
for (int i = 1; i <= n; i++) printf("%d ", topo[i]);
}
总代码
完整代码
#include <iostream>
#include <cstdio>
#include <queue>
#define N 233
#define M 2005
using namespace std;
int head[N], cur;
struct Node
{
int now, nxt;
}e[M];
void add(int x, int y)
{
e[++cur].now = y;
e[cur].nxt = head[x];
head[x] = cur;
}
int n, m;
int in[N]; //入度
int topo[N];
bool topo_sort()
{
cur = 0; //再次利用
queue <int> Q;
for (int i = 1; i <= n; i++)
if (in[i] == 0)
Q.push(i);
while (!Q.empty())
{
int x = Q.front();
Q.pop();
topo[++cur] = x;
for (int i = head[x]; i; i = e[i].nxt)
{
int t = e[i].now;
in[t]--;
if (in[t] == 0) Q.push(t);
}
}
return (cur == n);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int x, y;
scanf("%d%d", &x, &y);
add(x, y); //x->y
in[y]++;
}
bool chk = topo_sort();
if (chk == false) printf("This graph isn't a DAG."); //图存在环
else
{
for (int i = 1; i <= n; i++) printf("%d ", topo[i]);
}
return 0;
}
测试样例:
6 8
1 2
1 3
1 4
3 2
3 5
4 5
6 4
6 5
有关『队列』的思考
其实,此处必须用队列吗?
了解了拓扑排序的原理后,很容易发现,不一定用队列。
比如,可以使用栈。
观察一下两种数据结构的不同答案。
很明显,两种序列都是对的,因为前文提到过,拓扑序列并非唯一。
不过,两个序列并没有什么明显的规律呀!
有些题目会让你求:
一行,输出拓扑序列。
如果不存在拓扑序列,输出
。
如果有多个拓扑序列,输出字典序最小的那一个。
此时,你想到了什么?
没错!优先队列!
如果我们使用优先队列,就可以让拓扑序列相对有序了。
我们再列一个表格。
是不是稍微有一点规律了?!
此外,如果你给优先队列重载,就可以达到不同的需求了。
如果你重载成随机 true false,你就可以得到多个不同的拓扑序列了!
结语 & 参考文献
拓扑排序其实挺简单的,对吧!
参考了《算法训练营 海量题解+竞赛刷题:入门篇》一书,样例参照了此书。
本文的图均为作者所制,未经允许不可转载。图使用 https://csacademy.com/app/graph_editor/ 绘画。
前台所有表格都炸了,只好在后台截图。
今后会在本文加入例题的!
首发:2022-05-15 22:06:31
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用