题解 P4017 【最大食物链计数】

· · 题解

好久没写题解了,在这里水一波
update 2019.11.15 优化代码风格,增加正确性证明&注释
update 2020.1.6 使题解满足新版规定
update 2020.6.26 再次更新 update 2024.1.6 再次更新使题解满足规定

总的思路:拓扑排序 (管理员别以为我的题解和楼下的一样)

这里具体讲一下为什么要用拓扑排序(思维过程):

1、这是一道图论题;

2、不是求最短路;

3、根据提示“最左端是不会捕食其他生物的生产者”可以想到,我们要入度为零的点开始查找;

4、再看一遍题目,就是求路径数,当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队;

5、出度为零的点的路径总数和就是答案。

思路已经呼之欲出了:拓扑排序!

下面讲讲如何实现。

拓扑排序的精髓就在于每个点只会入队一次,每条边只会通过一次,所以时间复杂度就有很好的保证,O(N+M),SPFA 的玄学时间复杂度)。

正确性说明:题目的补充说明告诉我们这是一张 DAG(有向无环图),因此必定存在一个入度为 0 的点,也因此每一个点都会被遍历。

下面的代码的变量解释:

$h_i$ 表示在可以直接吃掉 $i$ 的所有关系中最后的一条的编号(邻接矩阵用); $ru_i$ 表示 $i$ 的入度,是整个程序的核心数组; $mp_{i,j}$ 表示 $j$ 是否能直接吃掉 $i$; $chu_i$ 表示 $i$ 的出度; 结构体 AB 记录 $m$ 条关系。 当一个点的入度变为零,即所有它能吃的东西都已经搜索过了,这是它的数值就不会发生变化,就可以入队了。这样保证了队列里的所有数值都不会发生变化。 --- 实现方式有以下两种: 方法一: 观察到数据范围 $n \le 5000$,用计算器一摁,发现不会 MLE ,于是就可以大胆地开一个二维数组(即邻接矩阵)存储吃与被吃的关系。代码如下: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,ru[5005],chu[5005],a,b,f[5005],ans; int mp[5005][5005]; queue<int> q; int main(){ cin>>n>>m; for(int i=1;i<=m;i++){ scanf("%d%d", &a, &b); mp[a][b]=1;//记录关系 chu[a]++; ru[b]++;//记录入度和出度 } for(int i=1;i<=n;i++){ if(ru[i]==0) { f[i]=1; q.push(i);//入度为零的入队 } } while(!q.empty()){//队列不为空 int a=q.front(); q.pop();//出队 for(int k=1;k<=n;k++){ if(mp[a][k]==0)continue; f[k]+=f[a];//更新 f[k]%=80112002; ru[k]--;//食物少了一个 if(ru[k]==0){//入队为零才入队 if(chu[k]==0){ ans+=f[k]; ans%=80112002; continue;//有没有都行 } q.push(k); } } } cout<<ans; } ``` 实测时空规模:598ms / 94.18MB (在 MLE 的边缘疯狂试探) 方法二: 用邻接表存储吃与被吃的关系,其他的和上一个代码几乎一模一样。代码如下: ```cpp #include<bits/stdc++.h> using namespace std; const int mod=80112002; int n, m, h[5005], ru[5005], chu[5005], f[5005], ans; struct AB{ int a,b,n; }d[5000005]; queue<int> q; int main(){ cin>>n>>m; for(int i=1;i<=m;i++){ int a, b; scanf("%d%d", &a, &b); d[i].a=a, d[i].b=b, d[i].n=h[a], h[a]=i;//建图 chu[a]++, ru[b]++; } for(int i=1;i<=n;i++){ if(ru[i]==0) { f[i]=1; q.push(i); } } while(!q.empty()){ int a=q.front(); q.pop(); for(int k=h[a];k;k=d[k].n){ int b=d[k].b; f[b]+=f[a]; f[b]%=mod; ru[b]--; if(ru[b]==0){ if(chu[b]==0){ ans+=f[b]; ans%=mod; }//出度为0的点为食物链终点,记录答案,并且不必入队 else q.push(b); } } } cout<<ans; } ``` 实测时空规模: 225ms / 7.70MB 多说几句:优化方法: 1、开O2优化 204ms / 6.65MB 2、读入优化 111ms / 7.01MB 3、膜拜AK IOI的巨佬 请亲测