拓扑排序Topological Sort

本文介绍了拓扑排序在解决动态规划问题中的应用,特别是在计算最大食物链数量时的重要性。通过无后效性和拓扑排序的原则,解释了如何设计动态规划状态并利用拓扑排序进行求解,提供了Kahn算法和DFS算法的实现方法。

拓扑排序

拓扑排序用于有向无环图(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边深搜边正序标记可以,那么能不能回溯时标记呢?理论上也是可以的,但是还要再取反序,只是平时不用罢了

图的存储方式多种,拓扑排序也有两种主要实现方式,具体实现参照以上伪代码

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值