概念
拓扑排序
拓扑排序是一种在DAG上进行的算法。拓扑排序可以解决有若干对关系,需要求出满足所有关系的一种方案/最大值/最小值的问题。拓扑排序可以求出图中的一条拓扑序列。
拓扑排序可以检查出有向图中是否存在环,如果一个有向图存在合法的拓扑序列,说明该有向图内无环;反之,说明该有向图中有环。拓扑排序是无法给有向带环图求出拓扑序列的。
拓扑排序的时间复杂度是\(O(N + M)\),其中\(N\)为点数,\(M\)为边数。
拓扑序列
有若干条有向边(u, v)
,要求所有的u
均在线性序列中出现在对应的v
前,这样的序列称为拓扑序列。
形象地说,有若干件事情要做,做事件A前必须先完成事件B,做事件C前必须先完成事件D……拓扑序列就是做这些事情的顺序。
算法思路
核心思路
我们可以将一条有向图看做是一种约束关系,每个点的入度就是这个点被约束的次数。如果一个点被约束,则其不可以加入拓扑序列。
对于入度为0的点u
,u
此时没有约束,则其可以加入拓扑序列中。同时,因为u
已经加入拓扑序列,其带来的约束会消失。即删除每条u
连出的有向边(u, v)
(也就是删除约束关系),同时v
的入度会-1
。
对于一个点v
来说,若其入度不为0,说明v
还有对应的u
在约束,所以v
不能加入拓扑序列;若其入度为0,说明v
所有对应的u
均已加入拓扑序列,所以v
可以加入拓扑序列。因此,拓扑排序可以分为三步:
- 找出入度为0的点
u
,并将其加入拓扑序列。 - 删除
u
连出的每条有向边(u, v)
。 - 重复第一步,直到图中没有入度为0的点为止。
算法结束后,如果加入拓扑序列的顶点数 <
图中所有的顶点数,说明有向图中存在环;否则,拓扑排序已经求出了该图的一条拓扑序列。
如果需要求出字典序最小的拓扑序列,则存储的数据结构需要换成小根堆的优先队列。
建模思路
如果题目中给出了若干条约束关系:\(A\)必须与\(B\)……\(C\)必须与\(D\)……则这道题目大概率可以使用拓扑排序解决。使用拓扑排序的关键,在于正确的建图。
我们可以把一条有向边看做是一种关系。例如,如果u
的点权 <
v
的点权,我们可以将其看做是一条有向边(u, v)
。又如,\(NOIP 2013\)的\(T4\)车站分级一题中,我们将一条有向边(u, v)
看做是车站u
的级别 <
车站v
的级别。
建图后,拓扑排序的解法就很容易得出了。
拓扑排序非常适合与\(\textbf{DAGdp}\)进行搭配使用。因为进行状态转移的时候,要求其的前驱状态必须都已经计算完毕,与拓扑序列的性质不谋而合。因此,拓扑排序和记忆化搜索是实现\(DAGdp\)的最佳选择。
对于一个有向图\(G\),如果在拓扑排序的过程中,队列里出现了多个元素,说明图\(G\)的拓扑序不唯一,反之说明图\(G\)的拓扑序是唯一的。因为当一个元素入队后,其所有的前驱一定都已经出队。如果此时队列里有多个元素,则这些元素没有前驱的约束,在之后的拓扑序中的位置可以安排。
拓扑排序有两个非常重要的性质:一,假如在拓扑排序的过程中 ,队列里出现了多于一个元素,说明该图的拓扑序不唯一;二,假如拓扑序列的长度小于图中的顶点数量,说明图中有环。这两个性质对解题有极大的作用。
模板
#include <cstdio>
#include <queue>
using namespace std;
#define maxn 1005
#define maxm 100005
struct node
{
int to, nxt;
}edge[maxm];
int n, m, cnt;
int head[maxn], indeg[maxn], res[maxn];
queue<int> q;
void topo()
{
for (int i = 1; i <= n; i++)
if (!indeg[i])
q.push(i);
while (!q.empty())
{
int v = q.front();
q.pop();
res[++cnt] = v;
for (int i = head[v]; i; i = edge[i].nxt)
{
indeg[edge[i].to]--;
if (!indeg[edge[i].to])
q.push(edge[i].to);
}
}
}
int main()
{
int x, y;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
scanf("%d%d", &x, &y);
edge[i].to = y;
edge[i].nxt = head[x];
head[x] = i;
indeg[y]++;
}
topo();
for (int i = 1; i <= cnt; i++)
printf("%d ", res[i]);
printf("\n");
return 0;
}
例题选讲
最长链问题
给定一个有向无环图\(G\),试求\(G\)中最长链的长度。
这道题是一个经典的\(DAGdp\)问题。令\(dp_{i}\)表示以顶点\(i\)结尾的最长链的长度。我们在拓扑排序的时候可以顺便进行状态转移。因为如果拓扑排序的过程中,某个顶点\(u\)入度已经为\(0\),说明与\(u\)相连的顶点的\(dp\)值都已经计算完毕。此时,以\(u\)结尾的最长链的长度等于所有相邻顶点的最长链长度加一,即\(dp_{u} = max(dp_{v} + 1), (u, v) \in E\)。最终取最大值就是该有向无环图的最长链长度。