拓扑排序学习笔记

前言

在学习这篇文章之前,你需要了解的算法有:

  1. 基本图论知识
  2. 链式前向星(图的一种存储方式)
  3. 了解 队列、栈 等简单数据结构的实现,用 STL 也行。

什么是拓扑排序

AOV网的定义

在了解拓扑排序(topo sort)之前,我们还得了解一个东西:AOV 网

AOV 网(Activity On Vertex) 主要用于表示活动间的优先关系。

看下面一个 非常不 生动的例子。

假设你在玩游戏,你想要解锁一个宝箱。

想要解锁这个宝箱,你需要先完成一堆任务,而每个任务又需要先完成子任务后才能解锁。

具体如下:

那么 AOV 网长啥样呢?

先观察这个 AOV 网,自己寻找规律。

现在,请思考一下 AOV 网是如何创建的。

(思考ing)

思考完了吧,其实规律是很好找的,就一句。

  • 如果 \(x \to y\),说明完成任务 \(y\) 之前要先完成任务 \(x\)

比如,想完成 \(4\),必须先完成 \(1\)\(6\)

看一下图,\(6\)\(1\) 是不是的确有一条边连向 \(4\) 呢?

进一步讲:

  • 如果存在一条路径使得 \(x\) 通向 \(y\),则完成任务 \(y\) 之前要先完成任务 \(x\)

不过这一条件并没有太多作用。

以上就是 AOV 网大致的规律了,需要注意的是:

  • 如果图上存在环,必定没有办法完成全部任务。
  • 即,AOV 网必为一个 DAG(Directed Acycline Graph,有向无环图)。

拓扑排序的定义

说了这么一大坨,终于可以开始讲拓扑排序是神马东西了。

其实这东西特别简单,就是求一个序列,满足:

  • 按照序列的顺序完成任务,可以将全部任务完成

需要注意的是:

  • 拓扑序列应该为 \(1\)\(n\) 的其中一个全排列。
  • 如果图上存在环,将没有拓扑序列(这一点前文提到过)。
  • 拓扑序列一般有多个,但是程序通常只能求其中一个

而拓扑排序,就是求出这个拓扑序列的。

下面,我们正式讲解拓扑排序的实现。

思路实现

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

准备一个队列,并将所有入度为 \(0\) 的点压入队列。

代码:

int cur = 0;
queue <int> Q;
for (int i = 1; i <= n; i++)
    if (in[i] == 0)
        Q.push(i);

3

每次都将队首计入 topo[] 数组中。

然后遍历所有队首点可以一次到达的点,如 \(1\) 要遍历到 \(2\)\(3\)\(4\)

(正是因为这一点,我们采用链式前向星)

然后将遍历到的点 in[x]--

若当前点入度被减成 \(0\) 了,说明这个任务可以执行了,即:将当前点压入队列。

一直执行,直到队列为空。

代码:

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

最后,我们要判断是否存在拓扑序列。

很简单,不存在拓扑序列,当且仅当 \(\text{拓扑序列元素个数} < n\)

为什么呢?

看这张图,显然是一个普通的环。

所有点入度都不为 \(0\),故不存在拓扑序列。

那如果外面还有一些点呢?

此处的点 \(a\)\(b\)\(c\)\(d\) 之外可能还有其他点。

假设 点 \(a\)\(b\)\(c\)\(d\) 已经入队了。

你会发现,点 \(a\)\(b\)\(c\)\(d\) 仅仅能处理当前的边,所以最后仍然会留下这个环。

综上所述,可以轻松判断拓扑序列是否符合要求。

代码:

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

有关『队列』的思考

其实,此处必须用队列吗?

了解了拓扑排序的原理后,很容易发现,不一定用队列。

比如,可以使用

观察一下两种数据结构的不同答案。

很明显,两种序列都是对的,因为前文提到过,拓扑序列并非唯一。

不过,两个序列并没有什么明显的规律呀!

有些题目会让你求:

一行,输出拓扑序列。

如果不存在拓扑序列,输出 \(-1\)

如果有多个拓扑序列,输出字典序最小的那一个。

此时,你想到了什么?

没错!优先队列!

如果我们使用优先队列,就可以让拓扑序列相对有序了。

我们再列一个表格。

是不是稍微有一点规律了?!

此外,如果你给优先队列重载,就可以达到不同的需求了。

如果你重载成随机 true false,你就可以得到多个不同的拓扑序列了!

结语 & 参考文献

拓扑排序其实挺简单的,对吧!

参考了《算法训练营 海量题解+竞赛刷题:入门篇》一书,样例参照了此书。

本文的图均为作者所制,未经允许不可转载。图使用 https://csacademy.com/app/graph_editor/ 绘画。

前台所有表格都炸了,只好在后台截图。

今后会在本文加入例题的!

首发:2022-05-15 22:06:31

posted @ 2022-08-26 01:47  liangbowen  阅读(53)  评论(0编辑  收藏  举报