简述
拓扑排序应用于有向无环图(DAG)之中,当且仅当一个有向图为有向无环图时,才能得到对应于该图的拓扑排序。每一个有向无环图都至少存在一种拓扑排序。
排序完以后会出现这样的性质:对于一个点p,只对排序位置在它之后的点有边。如果有环,则环上的点以及环上点所能到达的点都不会被放进拓扑序列中。
举例来说,如果我们将一系列需要运行的任务构成一个有向图,图中的有向边则代表某一任务必须在另一个任务之前完成这一限制。那么运用拓扑排序,我们就能得到满足执行顺序限制条件的一系列任务所需执行的先后顺序。当然也有可能图中并不存在这样一个拓扑顺序,这种情况下我们无法根据给定要求完成这一系列任务,这种情况称为循环依赖(circular dependency)。
拓扑排序的理解:一项大的工程可以看作是由若干个称为“活动”的子工程组成的集合,这些子工程之间必定存在一种先后关系,即某些子工程必须在其它一些工程完成之后才能开始,我们可以用有向图表示工程间关系,子工程(活动)为顶点,活动之间的先后关系为有向边
所以,拓扑排序一般用于有先后依赖关系任务的处理。而且通常与递推关联密切,从而解决例如方案数、最优性等问题。拓扑排序也可以判断一个图是否为DAG。
具体实现:记录节点入度,每次寻找入度为0的点加入队列,遍历与之相连的点,更新入度,并做累加、递推、统计等对答案有用的操作。
例题
洛谷P4017最大食物链计数
分析
设
f
[
i
]
f[i]
f[i] 为到第
i
i
i个点时食物链的最大数量。
显然
f
[
e
[
i
]
.
t
o
]
=
f
[
q
[
h
]
]
+
1
f[e[i].to]=f[q[h]]+1
f[e[i].to]=f[q[h]]+1
剩下的直接暴力拓扑即可(食物链之间相当于先后关系)
上代码
P3183 [HAOI2016]食物链 【双倍经验】
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
struct node
{
int to,next;
}e[500010];
const int mod=80112002;
int n,m,ans;
int in[5010],f[5010],out[5010];
int hd[500010],tot;
void add(int x,int y)
{
e[++tot]=(node){y,hd[x]};
hd[x]=tot;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
in[y]++;
out[x]++;
add(x,y);
}
queue<int> q;
for(int i=1;i<=n;i++)
{
if(!in[i])
{
q.push(i);
f[i]=1;
}
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=hd[x];i;i=e[i].next)
{
f[e[i].to]=(f[e[i].to]+f[x])%mod;
in[e[i].to]--;
if(in[e[i].to]==0)
{
if(out[e[i].to]==0) ans=(ans+f[e[i].to])%mod,f[e[i].to]%=mod;
else q.push(e[i].to);
}
}
}
cout<<ans%mod;
return 0;
}
洛谷P1137 旅行计划
分析
“自西向东”也就是先后依赖关系,考虑拓扑排序+递推。显然,一定从入度为0的点(最西边)出发才能是最多的。
设
f
[
i
]
f[i]
f[i] 为以
i
i
i为终点最多能游览的城市数量,可以得到
f
[
e
[
i
]
.
t
o
]
=
f
[
q
[
h
]
]
+
1
f[e[i].to]=f[q[h]]+1
f[e[i].to]=f[q[h]]+1
上代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
struct node
{
int to,next;
}e[200010];
int n,m,in[100010],f[100010];
int tot,hd[200010];
void add(int x,int y)
{
e[++tot]=(node){y,hd[x]};
hd[x]=tot;
}
void topsort()
{
queue<int> q;
for(int i=1;i<=n;i++)
{
if(!in[i])
{
q.push(i);
f[i]=1;
}
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=hd[x];i;i=e[i].next)
{
f[e[i].to]=f[x]+1;
in[e[i].to]--;
if(!in[e[i].to]) q.push(e[i].to);
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
in[y]++;
add(x,y);
}
topsort();
for(int i=1;i<=n;i++)
{
cout<<f[i]<<endl;
}
return 0;
}
洛谷P1983 车站分级
分析
车站之间的“停与不停”也是具有依赖关系的,所以还是考虑拓扑+递推。
在起终点站中间循环看哪个没有停,那么这几个站都要比没停的站高一级。所以就让中间没停的站向停了的站连边,每一轮都是这样,注意连边的时候用邻接矩阵会好一点,不然会有重边然后入度就会多加一点。
对于连好的图进行拓扑排序,设 f [ i ] f[i] f[i]为第 i i i个站点的等级,入度为0的节点也就是最低级的站,等级为1,显然有 f [ i ] = f [ q [ h ] ] + 1 f[i]=f[q[h]]+1 f[i]=f[q[h]]+1,答案就是 m a x ( f [ i ] ) max(f[i]) max(f[i])。
上代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
int n,m,v[1010],a[1010],in[1010],mx,f[1010];
int g[1010][1010];
void topsort()
{
memset(v,0,sizeof(v));
queue<int> q;
for(int i=1;i<=n;i++)
{
if(!in[i])
{
f[i]=1;
q.push(i);
v[i]=1;
}
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=1;i<=n;i++)
{
if(g[x][i]&&!v[i])
{
in[i]--;
f[i]=f[x]+1;
mx=max(mx,f[i]);
if(!in[i])
{
q.push(i);
v[i]=1;
}
}
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
memset(v,0,sizeof(v));
int x;
scanf("%d",&x);
for(int j=1;j<=x;j++)
{
scanf("%d",&a[j]);
v[a[j]]=1;
}
for(int j=a[1];j<=a[x];j++)
{
if(!v[j])
{
for(int k=1;k<=x;k++)
{
if(!g[j][a[k]])
{
g[j][a[k]]=1;
in[a[k]]++;
}
}
}
}
}
topsort();
cout<<mx;
return 0;
}