一、实际生活中的问题
在日常生活中,一项大的工程可以看作是由若干个子工程组成的集合,这些子工程之间必定存在一些先后关系,即某些子工程必须在其它一些子工程完成之后才能开始。
我们可以用有向图来表示这些子工程之间的先后关系:子工程为顶点,子工程之间的先后关系为有向边,这种有向图称为“顶点活动网络”,又称“AOV 网”。一个AOV 网应该是一个有向无环图(Directed Acyclic Graph,DAG),否则必定会有一些活动互相牵制,造成环中的活动都无法进行。
二、WHAT IS 拓扑排序
在AOV 网中,所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面。
对一个DAG G = (V, E) 进行拓扑排序,是将G 中所有顶点排成一个线性序列,使得图中任意一对顶点u 和v,若边u —> v ∈ E,则u 在线性序列中出现在v 之前。
由某个集合上的一个偏序得到该集合上的一个全序,这个操作称为拓扑排序。所得的线性序列,称为拓扑序。
三、算法流程
1. 在有向图中选一个没有前驱的顶点并且输出;
2. 从图中删除该顶点和所有它指出的有向边;
3. 重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的。因此,也可以通过拓扑排序来判断一个图是否有环。
四、例题与代码实现
模板题1 比赛 有n个比赛队进行比赛,给出m场比赛的结果,每场都形如P1赢P2(排名时P1在P2之前)。你需要确定排名。由于排名可能不唯一,你需要给出字典序最小的排名方案。输入第一行,两个整数,n和m。接下来m行,每行两个整数P1, P2。输出共一行,n个整数,表示字典序最小的排名。
SOL:若是P1赢P2,就连一条从P1到P2的边,使用小根堆(优先队列)进行拓扑排序即可。

#include <cstdio> #include <queue> using namespace std; int n,m,deg[100010]; struct DAGedge { int next,to; }edge[200010]; int head[100010],edge_num; void add(int from,int to)//链式前向星存边 { edge[++edge_num].next=head[from]; edge[edge_num].to=to; head[from]=edge_num; } priority_queue<int,vector<int>,greater<int> > q;//由于按照字典序输出,应使用优先队列实现拓扑排序 int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y); deg[y]++; }//读入并存边 for(int i=1;i<=n;i++) if(deg[i]==0) q.push(i);//将入度为0的点加入队列 while(!q.empty()) { int t=q.top(); q.pop(); printf("%d ",t);//取出字典序最小的点并输出 for(int i=head[t];i;i=edge[i].next)//枚举与其相连的所有边 { deg[edge[i].to]--;//将这些边的入度-1,相当于删去了这些边 if(deg[edge[i].to]==0) q.push(edge[i].to);//如果该点的入度变为0则加入队列 } } return 0; }
模板题2 【NOIp提高组 2003】神经网络

#include <cstdio> #include <stack> #pragma optimize (2) #define debug() printf("我喜欢李靖妍") using namespace std; stack <int> sta; int kase[1005], head[1005], tot, n, p, indeg[1005]; bool ok = false; struct edge { int u, v, next, w; } e[100005]; void addedge(int from, int to, int value) { e[++tot].u = from; e[tot].v = to; e[tot].w = value; e[tot].next = head[from]; head[from] = tot; } void toposort() { while (!sta.empty()) { int u = sta.top(); sta.pop(); if (kase[u] <= 0) { for (int i = head[u]; i; i = e[i].next) { indeg[e[i].v]--; if (!indeg[e[i].v]) sta.push(e[i].v); } } else { for (int i = head[u]; i; i = e[i].next) { indeg[e[i].v]--; kase[e[i].v] += kase[u] * e[i].w; if (!indeg[e[i].v]) sta.push(e[i].v); } } } } int main() { scanf("%d%d", &n, &p); for (int i = 1; i <= n; i++) { int que; scanf("%d%d", &kase[i], &que); if (kase[i] != 0) sta.push(i); else kase[i] -= que; } for (int i = 1; i <= p; i++) { int a, b, c; scanf("%d%d%d", &a, &b, &c); addedge(a, b, c); indeg[b]++; } toposort(); for (int i = 1; i <= n; i++) { if (!head[i] && kase[i] > 0) { printf("%d %d\n", i, kase[i]); ok = true; } } if (!ok) printf("NULL"); return 0; }
此题建议使用栈来实现。
实战题1 某公司有n员工,这n个员工共有m个要求,每个要求表示第x个人的工资比第y个人的高。每个人的工资都是整数,且至少为888。问老板发出的总工资最少是多少。
输入
第一行,两个整数,n和m。
接下来m行,每行两个整数x, y,表示第x个人的工资比第y个人的高。
输出
共一行,一个整数,表示最少的总工资。
样例
输入
2 1 1 2
输出
1777
数据范围
n≤10000, m≤20000
SOL:由于最低工资有限制,我们肯定从小往大推。对于ax > ay 的限制,建边y -> x,进行拓扑排序,那么每次取出入度为0 的点,用它的工资+1 去更新后继节点。

#include <cstdio> #include <iostream> #include <cmath> #include <algorithm> #include <cstring> #include<string> #include<queue> using namespace std; int n,m,ru[10010],money[10010]; struct hh { int next,to; }edge[20010]; int head[10010],edge_num; void add(int from,int to) { edge[++edge_num].next=head[from]; edge[edge_num].to=to; head[from]=edge_num; } queue<int> q; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); add(y,x); ru[x]++; } for(int i=1;i<=n;i++) if(ru[i]==0) {q.push(i);money[i]=888;} int ans=0; while(!q.empty()) { int t=q.front(); ans+=money[t]; q.pop(); for(int i=head[t];i;i=edge[i].next) { ru[edge[i].to]--; if(ru[edge[i].to]==0) {money[edge[i].to]=money[t]+1;q.push(edge[i].to);} } } printf("%d",ans); return 0; }
实战题2 【NOIp普及组 2013】车站分级
然而我作死用线段树维护拓扑排序

#include <cstdio> #define nm 1501 using namespace std; int h[nm << 3], n, m, s[nm], ind[nm << 3], num[nm]; int dis[nm << 3], depth[nm << 3], now, topo, q[nm << 5]; int head, talow, count, ans; struct edge { int to; int next; }e[nm * 699]; inline char getchars() { static char buf[100000], * p1 = buf, * p2 = buf; return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++; } inline int re() { int x = 0, fl = 1; char ch = getchars(); for (; ch < 48 || ch>57; ch = getchars()) if (ch == '-') fl = -1; for (; 48 <= ch && ch <= 57; ch = getchars()) x = (x << 3) + (x << 1) + (ch ^ 48); return x * fl; } inline void plus(int a, int b) { ind[b]++; e[++now] = (edge){ b, h[a] }; h[a] = now; } inline int ls(int x) { return x << 1; } inline int rs(int y) { return y << 1 | 1; } inline void make(int ljy, int l, int r) { if (ljy > topo) topo = ljy; if (l == r) { num[l] = ljy; dis[ljy] = 1; return; } int mid = (l + r) >> 1; make(ls(ljy), l, mid); plus(ls(ljy), ljy); plus(rs(ljy), ljy); make(rs(ljy), mid + 1, r); } inline void ud(int ljy, int l, int r, int x, int y, int tmp) { if (x <= l && r <= y) { plus(ljy, tmp); return; } int mid = (l + r) >> 1; if (mid >= x) ud(ls(ljy), l, mid, x, y, tmp); if (y > mid) ud(rs(ljy), mid + 1, r, x, y, tmp); } inline void toposort(int a) { for (int i = 1; i <= topo; i++) if (!ind[i]) { q[talow++] = i; depth[i] = dis[i]; } while (head < talow && a) { int u = q[head++]; for (int i = h[u]; i; i = e[i].next) { int v = e[i].to; if (depth[u] + dis[v] > depth[v]) depth[v] = depth[u] + dis[v]; ind[v]--; if (!ind[v]) q[talow++] = v; } } } int main() { n = re(); m = re(); make(1, 1, n); for (int i = 1; i <= m; i++) { count = re(); topo++; for (int j = 1; j <= count; j++) s[j] = re(); for (int j = 1; j < count; j++) { plus(topo, num[s[j]]); if (s[j] + 1 <= s[j + 1] - 1) ud(1, 1, n, s[j] + 1, s[j + 1] - 1, topo); } plus(topo, num[s[count]]); } toposort(1); for (int i = 1; i <= n; i++) if (depth[num[i]] > ans) ans = depth[num[i]]; printf("%d", ans); return 0; }
实战题3 给定一个DAG,含有n个点和m条有向边。你需要计算极长有向路径的数目。如果一条路径u1→u2→⋯→uk满足:对于1≤i<k,边ui→ui+1∈E; 原图中,u1u1没有前驱节点、uk没有后继节点,那么这条路径就是极长有向路径。单点不算极长有向路径。
格式输入
第一行,两个整数n, m。接下来mm行,每行两个整数u, v,表示一条u到v的有向边。
输出
共一行,一个整数,表示极长有向路径的数目,保证在int范围内。
样例输入
10 16 1 2 1 4 1 10 2 3 2 5 4 3 4 5 4 8 6 5 7 6 7 9 8 5 9 8 10 6 10 7 10 9
输出
9
数据范围
n≤105, m≤2×105
需要使用加法原理emmm

#include <cstdio> #include <iostream> #include <cmath> #include <algorithm> #include <cstring> #include<string> #include<queue> using namespace std; int n,m,ru[100010],num[100010]; struct hh { int next,to; }edge[200010]; int head[100010],edge_num; void add(int from,int to) { edge[++edge_num].next=head[from]; edge[edge_num].to=to; head[from]=edge_num; } queue<int> q; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y); ru[y]++; } for(int i=1;i<=n;i++) if(ru[i]==0 && head[i]) {q.push(i);num[i]=1;} int ans=0; while(!q.empty()) { int t=q.front(); if(!head[t]) ans+=num[t]; q.pop(); for(int i=head[t];i;i=edge[i].next) { ru[edge[i].to]--; num[edge[i].to]+=num[t]; if(ru[edge[i].to]==0) {q.push(edge[i].to);} } } printf("%d",ans); return 0; }
实战题4
你需要生成一个n的排列,有m个要求。第i个要求长为ki,有ki个数ai,1,ai,2,…,ai,ki, 表示在最终的排列里,这ki个数要按照这个顺序组成一个子序列(不一定要连续)。
因为很可能不是所有的要求都能满足,你要最大化M,使得前M个要求都能被满足。在这个前提下,排列的字典序最小。
格式输入
第一行,两个整数n和m。接下来m行,每行描述一个要求:第一个数是ki,接下来ki个整数ai,j,含义如题目所述。
输出 共一行,n个整数,表示满足要求的字典序。
样例输入
4 3 3 1 2 3 2 4 2 3 3 4 1
输出
1 4 2 3
数据范围
n≤105, ∑ki≤2×105
思路:最大化M 可以通过二分答案来做。把一个要求拆成ki- 1 个要求(有向边),拓扑排序即可。

#include<iostream> #include<cstdio> #include<queue> using namespace std; int n,m; struct edge { int from,to,nxt; } e[200010]; int cnt; struct point { int to,deg,fir,num; } p[100010]; bool operator < (point a,point b) { return a.num>b.num; } priority_queue <point> pq; void add(int from,int to,int num) { e[num].nxt=p[from].fir,p[from].fir=num,p[to].deg++; return; } int asd[100010]; int ans1[100010],ans2[100010]; void init(int tim) { for(int i=1;i<=asd[tim];i++) { e[i].nxt=0; } for(int i=1;i<=n;i++) { p[i].deg=0,p[i].fir=0; } for(int i=1;i<=asd[tim];i++) { add(e[i].from,e[i].to,i); } return; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) p[i].num=i; for(int i=1;i<=m;i++) { int ki; scanf("%d",&ki); asd[i]=asd[i-1]+ki-1; int tmp; scanf("%d",&tmp); for(int j=2;j<=ki;j++) { int tmp2; scanf("%d",&tmp2); cnt++; e[cnt].from=tmp,e[cnt].to=tmp2; tmp=tmp2; } } int l=0,r=m; while(l<r) { int cnt2=0; int mid=(l+r)/2; init(mid); for(int i=1;i<=n;i++) { if(!p[i].deg) { pq.push(p[i]); } } while(!pq.empty()) { point tmp=pq.top(); pq.pop(); cnt2++; ans2[cnt2]=tmp.num; for(int q=tmp.fir;q;q=e[q].nxt) { p[e[q].to].deg--; if(!p[e[q].to].deg) { pq.push(p[e[q].to]); } } } if(cnt2==n) { for(int i=1;i<=n;i++) { ans1[i]=ans2[i]; } l=mid+1; } else { r=mid; } } for(int i=1;i<=n;i++) { printf("%d ",ans1[i]); } return 0; }
实战题5
对一个有n个点、m条边的DAG拓扑排序。
定义一个拓扑序{pi}的位置数列{qi}为: ,换言之,qj表示数j在拓扑序中的位置。你需要给出位置数列字典序最小的方案。
格式输入 第一行,两个整数,n和m。接下来m行,每行两个整数u, v,表示有一条u到v的有向边。
输出 共一行,nn个整数,表示位置数列字典序最小的方案。注意,你需要输出的是拓扑序,而不是位置数列。
样例输入1
3 1 3 1
输出1
3 1 2
输入2
6 4 6 3 3 1 5 4 4 2
输出2
6 3 1 5 4 2
数据范围
对于100%的数据,n≤105, m≤2×105

#include<bits/stdc++.h> using namespace std; const int maxn=100010; const int maxm=200010; int n,m,u,v; int head[maxn],nxt[maxm],to[maxm],in[maxn]; int tot; stack<int> st; void add(int x,int y){ to[++tot]=y; nxt[tot]=head[x]; head[x]=tot; } void topo(){ priority_queue<int> q; for (int i=1;i<=n;i++){ if (in[i]==0) q.push(i); } while (!q.empty()){ int x=q.top(); q.pop(); st.push(x); for (int i=head[x];i;i=nxt[i]){ in[to[i]]--; if (in[to[i]]==0) q.push(to[i]); } } } int main(){ scanf("%d%d",&n,&m); for (int i=1;i<=m;i++){ scanf("%d%d",&u,&v); add(v,u); in[u]++; } topo(); while (!st.empty()){ printf("%d ",st.top()); st.pop(); } }
实战题6
给定一个DAG,有n个点、m条有向边,且每条有向边均从编号小的节点指向编号大的节点。图G还满足,对于1≤i<n,i号点一定能够到达n号点。有可能会有重边。如果一条路径u1→u2→⋯→uk满足:对于1≤i<k,边ui→ui+1∈E; 原图中,u1没有前驱节点、uk没有后继节点,那么这条路径就是极长有向路径。一条边的繁忙程度,定义为经过这条边的极长有向路径的数目,你需要找出最繁忙的边的繁忙程度。
格式输入 第一行,两个整数n, 。接下来m行,每行两个整数u, v,表示一条u到v的有向边。
输出 共一行,一个整数,表示最繁忙的边的繁忙程度,保证答案在int范围内。
样例输入
7 7 1 3 3 4 3 5 4 6 2 3 5 6 6 7
输出
4
数据范围
100%的数据满足n≤105, m≤2×105。

#include<cstdio> #include<iostream> #include<queue> using namespace std; int n,cnt=0,ans=0,head[500005],rd[500005],dp[500005]; struct Edge{ int v,w,nxt; }e[500005],e2[500005]; void addEdge(int u,int v,int w){ e[++cnt].v=v; e[cnt].w=w; e[cnt].nxt=head[u]; head[u]=cnt; } void topoSort(){ queue<int> q; for(int i=1;i<=n;i++){ if(rd[i]==0){ q.push(i); dp[i]=1; } } while(!q.empty()){ int nowValue=q.front();q.pop(); for(int i=head[nowValue];i;i=e[i].nxt){ rd[e[i].v]--; dp[e[i].v]+=dp[nowValue]; //cout<<"dp["<<e[i].v<<"]+=dp["<<nowValue<<"]"<<endl; ans=max(ans,dp[e[i].v]); if(rd[e[i].v]==0){ q.push(e[i].v); } } } } int cnt2=0,ans2=0,head2[500005],rd2[500005],dp2[500005]; void addEdge2(int u,int v,int w){ e2[++cnt2].v=v; e2[cnt2].w=w; e2[cnt2].nxt=head2[u]; head2[u]=cnt2; } void topoSort2(){ queue<int> q; for(int i=1;i<=n;i++){ if(rd2[i]==0){ q.push(i); dp2[i]=1; } } while(!q.empty()){ int nowValue=q.front();q.pop(); for(int i=head2[nowValue];i;i=e2[i].nxt){ rd2[e2[i].v]--; dp2[e2[i].v]+=dp2[nowValue]; //cout<<"dp2["<<e2[i].v<<"]+=dp2["<<nowValue<<"]"<<endl; ans2=max(ans2,dp2[e2[i].v]); if(rd2[e2[i].v]==0){ q.push(e2[i].v); } } } } int trueAns=0; void work(){ queue<int> q; for(int i=1;i<=n;i++){ if(rd[i]==0){ q.push(i); } } while(!q.empty()){ int nowValue=q.front();q.pop(); for(int i=head[nowValue];i;i=e[i].nxt){ rd[e[i].v]--; trueAns=max(trueAns,dp[nowValue]*dp2[e[i].v]); if(rd[e[i].v]==0){ q.push(e[i].v); } } } } int main(){ int m; scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ int ta,tb; scanf("%d%d",&ta,&tb); addEdge(ta,tb,1); addEdge2(tb,ta,1); rd[tb]++; rd2[ta]++; } topoSort(); topoSort2(); work(); printf("%d\n",trueAns); //printf("%d\n",ans2); return 0; }
[全剧终]
以上是史上最烂拓扑排序教程,如果想看巨佬的生花妙笔,请点击链接 会宁狐狸 陌阡 RandomName