拓扑排序
拓扑排序用于有向无环图(DAG),作用是给出一个序列,使得任何一条边总是起点比终点在序列中出现的位置靠前
拓扑排序原理是每次找到入度为0的点,把它放进拓扑排序的序列中,然后把这个点和这个点引出的边全部删掉(当然,一个图的拓扑序不是唯一的,只要起到功效就可以了)
这个排序在没碰见它的题之前就会觉得这毫无用处,举个例子,拓扑排序可以用于动态规划
动态规划需要满足些什么条件?无后效性
而拓扑排序像什么?过关斩将,“给出一个序列,使得任何一条边总是起点比终点在序列中出现的位置靠前”这句话恰好是无后效性的体现
例题:P4017 最大食物链计数
题目背景
你知道食物链吗?Delia 生物考试的时候,数食物链条数的题目全都错了,因为她总是重复数了几条或漏掉了几条。于是她来就来求助你,然而你也不会啊!写一个程序来帮帮她吧。
题目描述
给你一个食物网,你要求出这个食物网中最大食物链的数量。
(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)
Delia 非常急,所以你只有 11 秒的时间。
由于这个结果可能过大,你只需要输出总数模上 8011200280112002 的结果。
输入格式
第一行,两个正整数 n、m,表示生物种类 n 和吃与被吃的关系数 m。
接下来 m 行,每行两个正整数,表示被吃的生物A和吃A的生物B。
输出格式
一行一个整数,为最大食物链数量模上 8011200280112002 的结果。
输入输出样例
输入 #1复制
5 7 1 2 1 3 2 3 3 5 2 5 4 5 3 4
输出 #1复制
5
说明/提示
各测试点满足以下约定:

【补充说明】
数据中不会出现环,满足生物学的要求。(感谢 @AKEE )
这个题属于计数问题,并查集之类的直接排除掉,用树很麻烦,这是有向无环图,用深搜等价于拓扑排序
那么现在考虑动态规划的做法,因为计数问题可以dp
这个题问的是有多少条最长链,注意断句,分析样例数据可知,这不能用过的点就删掉,因为可以多条最长链共用同样的生产者部分,你可以看一下P1020 [NOIP1999 普及组] 导弹拦截,一般新手的朴素思路是每次挑出来最长链,然后剔除掉(虽然标准做法是使用Dilworth定理的),不过你可以比对一下,两道题是截然不同的
同时参考一下数据规模:

这个数据规模n平方很危险,应该只能接受 O ( n ) 或者 O (n log n)
那么按照标准流程,先设计状态吧,要注意满足无后效性,不能与路径挂钩,那么我们可以
1.记f(i)表示i这个生产者引出的全部食物链数目
2.记f(i)表示i这个消费者为止的全部食物链数目
两种思路都可以解出题目,这里我选择的是思路1
读者可以先把样例数据画出来,然后用这个思路做一遍

画出来之后就会发现答案浮现出来了,是数学当中最基础的加法原理
当时入门的时候,是如何讲解加法原理和乘法原理还记得吗?这完全与加法原理一致
接下来确定细节
确定边界条件:
1.对于入度为0的最高级消费者,它起始的食物链只有一条,它自己
2.对于出度为0的生产者,它起始它所在的整个食物链,只对它计数就能避免重复计数
接下来考虑如何消除后效性:
思考一下图中我用红色数字标注的顺序:

想知道为什么是这个更新f(i)的顺序吗?因为这张图的拓扑排序恰好是:1,2,3,4,5
(我这个思路1是需要拓扑序的反序标号的,因为最高级消费者是边界条件)
同时,拓扑排序的时间复杂度是O(n+m),符合题目,其中n是点数,m是边数,按照这个数据规模,绰绰有余
那么根据原理,可以粗略写出如下的代码,如要学会拓扑排序的其他实现方式,请移步后文
#include<cstdio>
using namespace std;
const long long mod=80112002;
int from[500001][500];int cnt_from[500001];
int to[500001][500];int cnt_to[500001];
long long f[500001];
int order[500001];
int medium[500001];
int book[500001];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
from[b][++cnt_from[b]]=a;
to[a][++cnt_to[a]]=b;
}
for(int i=1;i<=n;i++)
medium[i]=cnt_from[i];//backup copy
int cnt=0;
while(cnt<n)//Topological Sort
{
for(int i=1;i<=n;i++)
{
if(book[i]) continue;
if(medium[i]==0)
{
order[++cnt]=i;
book[i]=1;
for(int j=1;j<=cnt_to[i];j++)
{
medium[to[i][j]]--;
}
}
}
}
// for(int i=1;i<=n;i++) //test
// cout<<order[i]<<" ";
// cout<<endl;
for(int i=1;i<=n;i++) if(cnt_to[i]==0) f[i]=1;//initialize
for(int i=n;i>=1;i--)//solve
{
for(int j=1;j<=cnt_to[order[i]];j++)
{
f[order[i]]+=f[to[order[i]][j]];
f[order[i]]%=mod;
}
}
long long ans=0;
for(int i=1;i<=n;i++)//count the number from the producers
if(cnt_from[i]==0) {
ans+=f[i];
ans%=mod;
}
printf("%d",ans);
return 0;
}
另外,这个题比较玄学,数组开小了会TLE(当然也可能MLE)
还有,要记得开long long,同时对过程和答案取模
拓扑排序的实现方式
学栈或者队列的时候,你或许会认为百无一用是栈/队列
然而下述的两个算法,一个用队列,一个用栈
1.Kahn算法
上文介绍的就是本算法

2.DFS算法

dfs边深搜边正序标记可以,那么能不能回溯时标记呢?理论上也是可以的,但是还要再取反序,只是平时不用罢了
图的存储方式多种,拓扑排序也有两种主要实现方式,具体实现参照以上伪代码
本文介绍了拓扑排序在解决动态规划问题中的应用,特别是在计算最大食物链数量时的重要性。通过无后效性和拓扑排序的原则,解释了如何设计动态规划状态并利用拓扑排序进行求解,提供了Kahn算法和DFS算法的实现方法。
156





