目录
一、关于拓扑排序
1. 定义
对于任意一个DAG图(Directed Acyclic Graph,有向无环图),如果一个序列a由该图所有节点组成,并且对于任意一条边\((u,v)\),u在序列中的位置均在v的位置之前,我们称这样的序列为该图的拓扑序(Topological Order),求拓扑序的过程就叫做拓扑排序(Topological Sort)。
2. 实现流程
我们每次将图中入度为0的节点入队,每次由队列中的节点扩展,每次扩展将相邻点的入度减1。若扩展时相邻节点的入度为0,那么我们也将这个节点入队。
二、拓扑排序的应用
拓扑排序能够解决什么样的问题?
1. 图中是否存在环
如果图中存在环,那么在拓扑排序后,环内节点的入度一定不为0,且这些节点无法插入到队列中。
我们可以根据这个性质判断环。
void topo_sort(){ queue<int> q; for(int i=1;i<=n;i++){ if(!in[i]) q.push(i); } while(!q.empty()){ int u=q.front();q.pop(); ans[++cnt]=u; for(int e=first[u];e;e=next[e]){ int v=go[e]; if(!--in[v]) q.push(v); } } } //在main函数内 if(cnt!=n) printf("exist"); for(int i=1;i<=n;i++) if(!ans[i]) printf("exist"); //两种判环方式
2. 描述元素关系
例1:P1113 杂务
入门题。只有之前所有杂物完成后才会开始这一项杂物,而完成这些杂物的时间由花费最长时间的杂物决定。
只要每次更新最长时间就可以了。
#include<bits/stdc++.h> #define N 100100 using namespace std; int in[N],cost[N],dist[N]; int n,first[N],next[N],go[N],tot; inline void add_edge(int u,int v){ next[++tot]=first[u]; first[u]=tot; go[tot]=v; } inline void toposort(){ queue<int> q; for(int i=1;i<=n;i++){ if(!in[i]) q.push(i); dist[i]=max(dist[i],cost[i]); } while(!q.empty()){ int u=q.front(); q.pop(); for(int e=first[u];e;e=next[e]){ int v=go[e],w=cost[v]; in[v]--; dist[v]=max(dist[v],dist[u]+w); if(!in[v]) q.push(v); } } } int main() { scanf("%d",&n); for(int i=1,id,len,must;i<=n;i++){ scanf("%d%d",&id,&len); cost[id]=len; while(scanf("%d",&must)&&must){ add_edge(must,id); in[id]++; } } toposort(); int ans=0; for(int i=1;i<=n;i++) ans=max(ans,dist[i]); printf("%d",ans); return 0; }
例2:P3116 [USACO15JAN]约会时间Meeting Time
动态规划+拓扑排序,题目要求求出从始点1到终点n的共同最小时间。因此我们可以从1开始拓扑排序,当节点u指向v时,可以花费cb[e]
,ce[e]
的时间由u到v。动态转移方程为if(f[u][i]) f[v][i+cb[e](ce[e])]=1
。其中f代表在节点u时花费时间i能否到达。
但是入度为0的节点不一定只有1,当存在其他入度不为0的节点时,就会存在有些节点无法入队。这时我们先需要对其他点先进行一次拓扑排序,这样保证只有1入度为0。
#include<bits/stdc++.h> using namespace std; int n,m,tot,in[100010]; int first[100010],next[100010],go[100010],cb[100010],ce[100010]; int stb[110][20010],ste[110][20010]; inline void add_edge(int u,int v,int c,int d){ next[++tot]=first[u]; first[u]=tot; go[tot]=v; cb[tot]=c;ce[tot]=d; } inline void Deal_first(){ queue<int>q; for(int i=2;i<=n;i++) if(in[i]==0) q.push(i); while(!q.empty()){ int u=q.front(); q.pop(); for(int e=first[u];e;e=next[e]){ int v=go[e]; if(!--in[v]) q.push(v); } } } inline void Topo_sort(){ queue<int> q; q.push(1); while(!q.empty()){ int u=q.front(); q.pop(); for(int e=first[u];e;e=next[e]){ int v=go[e]; if(!--in[v]) q.push(v); for(int i=0;i<=10001;i++){//动态规划 if(stb[u][i]) stb[v][i+cb[e]]=1; if(ste[u][i]) ste[v][i+ce[e]]=1; } } } } int main() { scanf("%d%d",&n,&m); for(int i=1,u,v,c,d;i<=m;i++){ scanf("%d%d%d%d",&u,&v,&c,&d); add_edge(u,v,c,d); in[v]++; } Deal_first(); stb[1][0]=ste[1][0]=1; Topo_sort(); for(int i=0;i<=10000;i++){ if(stb[n][i]&&ste[n][i]){ printf("%d",i); return 0; } } printf("IMPOSSIBLE"); return 0; }
例3:P3243 [HNOI2015]菜肴制作
这看起来是一道单纯的拓扑排序题。我们只要用小根堆维护一下,每次取最小序号的菜来更新其他的菜不就可以了么?好吧,只要看看标签是紫题你就会发现事情没那么简单。事实上,我们可以轻而易举地举出反例:
\[条件<5,1>,<3,4>\]
开始只有3,5入度为0,这样先取3, 于是4入队,此时队列中为{4,5};
取出4后才会轮到5,最后取出1。这样得到顺序(3,4,5,1),显然答案是(5,1,3,4)。
原因在于题目要求在保证先后顺序的条件下要求序号越小的点排在越前面,此时的最优策略在于让尽量大的数字排在后面。因此我们可以建立一个反图,每次取出队列中最大的序号来更新,这样就能保证序号较小的数字排在前面。此外题目为多项数据读入,记得每次的初始化。
#include<bits/stdc++.h> #define N 100010 using namespace std; int t,m,n,tot,flag,in[N],ans[N]; int first[N],next[N],go[N],cnt; inline void add_edge(int u,int v){ next[++tot]=first[u]; first[u]=tot; go[tot]=v; } inline void toposort(){ priority_queue<int> q; for(int i=1;i<=n;i++) if(!in[i]) q.push(i); while(!q.empty()){ int u=q.top();q.pop(); ans[++cnt]=u; for(int e=first[u];e;e=next[e]){ int v=go[e]; in[v]--; if(!in[v]) q.push(v); } } } inline void clear_data(){ tot=0;flag=0;cnt=0; memset(first,0,sizeof(first)); memset(next,0,sizeof(next)); memset(go,0,sizeof(go)); memset(in,0,sizeof(in)); memset(ans,0,sizeof(ans)); } int main() { scanf("%d",&t); while(t--){ scanf("%d%d",&n,&m); clear_data(); for(int i=1,u,v;i<=m;i++){ scanf("%d%d",&u,&v); in[u]++; add_edge(v,u); } toposort(); if(cnt<n) printf("Impossible!\n"); else{ for(int i=cnt;i>=1;i--) printf("%d ",ans[i]); printf("\n"); } } return 0; }
例4:P1983 车站分级
题目要求求出最少要划分多少等级。列车只停在等级大于已停靠的车站上,所以将停靠的车站指向未停靠的车站(等级大的指向等级小的)。由拓扑排序计算该DAG图的层数即为答案。
#include<bits/stdc++.h> #define N 1020 using namespace std; int n,m,h[N],vis[N],link[N][N],in[N]; int q[N],tot,ans,p; inline void toposort(){ memset(vis,0,sizeof(vis)); do{//求DAG的层数 p=0;//p为节点度数为0的个数 for(int i=1;i<=n;i++) if(!in[i]&&!vis[i]) q[++p]=i,vis[i]=1; for(int i=1;i<=p;i++) for(int j=1;j<=n;j++) if(link[q[i]][j]) in[j]--,link[q[i]][j]=0; ans++;//不断累积层数 }while(p>0); } int main() { scanf("%d%d",&n,&m); for(int i=1,s;i<=m;i++){ scanf("%d",&s); memset(vis,0,sizeof(vis)); for(int j=1;j<=s;j++){ scanf("%d",&h[j]); vis[h[j]]=1; } for(int j=h[1];j<=h[s];j++){//枚举所有列车可能经过的节点 if(!vis[j])//不停靠的节点 for(int k=1;k<=s;k++){ if(!link[j][h[k]]){//建立关系 in[h[k]]++,link[j][h[k]]=1; } } } } toposort(); printf("%d",ans-1); return 0; }