拓扑排序
定义
对一个有向无环图(简称 AOV )G 进行拓扑排序,是将 G 中所有顶点排成一个线性序列 ,使得 图中任意一对顶点 u 和 v ,若边 (u,v)∈E(G) ,则u在线性序列中出现在v之前 。通常,这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。AOV 网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列
时间复杂度
对有 N 个顶点和 E 条弧的有向图而言,建立求各顶点的入度的时间复杂度为 O(E),建零入度顶点栈的时间复杂度为O(N),在拓扑排序过程中,若有向图无环,则每个顶点进一次栈、出一次栈,入度减 1 的操作在语句中总共执行 E 次,所以总的时间复杂度为 O(n+e)
操作步骤
规则 :在一个有向图中,对所有的节点进行排序要求没有一个节点指向它前面的节点
先统计所有节点的入度,对于入度为 0 的节点就可以分离出来,然后把这个节点指向的节点的入度 -1
一直做上述操作,直到所有的节点都被分离出来。
如果最后存在入度不为 0 的节点,那就说明有环,不存在拓扑排序,也就是很多题目的无解情况
图示
拓扑序列不唯一:上图还可能是 afcdbe
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const int N=1e5 +5 ;int n,m;int in[N];vector <int > vec[N];queue <int > q;vector <int > ans;void init () { for (int i=1 ;i<=n;i++) vec[i].clear(); while (!q.empty())q.pop(); ans.clear(); memset (in,0 ,sizeof in); } void topsort () { for (int i=1 ;i<=n;i++) if (in[i]==0 )q.push(i); int cnt=0 ; while (!q.empty()){ int p=q.front(); q.pop(); cnt++; ans.push_back(p); for (int j=0 ;j<vec[p].size();j++){ int v=vec[p][j]; in[v]--; if (in[v]==0 )q.push(v); } } if (cnt!=n)cout <<"No Answer!" <<endl ; else { for (auto x:ans) cout <<x<<" " ; cout <<endl ; } } signed main () { ios::sync_with_stdio(false ),cin .tie(0 ); while (cin >>n>>m){ init(); int x,y; for (int i=1 ;i<=m;i++){ cin >>x>>y; vec[x].push_back(y); in[y]++; } topsort(); } return 0 ; }
有些拓扑排序要求字典序最小什么的,那就把队列换成优先队列 就好了
应用
主要用来解决一些有向图中依赖解析问题。很抽象的话:目前看到的题就是解决类似一些小学的题,比如做家务有的任务存在先后顺序,有的可以同时完成,让你安排时间使效率最高
模板题 :春节到了叔叔想给手下的 n
个工人发奖金,每个人至少要领到888元,其中有m
个条件:a
领到的钱至少要比 b
多,叔叔比较抠门想尽可能支出少的钱,让你帮他解决,能满足所有条件输出需要的钱不能输出 -1
思路:没要求的直接给 888 元就可以了,至于 a
领到的钱至少要比 b
多说明 a
有额外的要求,贪心就给 a
在b
的奖金基础上额外多发 1 元就好,如此就可以使用拓扑排序解决。反向建边然后另开一个 val[]
记录每个节点额外需要的钱,即每个节点的钱为上一个节点的 val+1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const int N=1e5 +5 ;int n,m;int in[N];vector <int > vec[N];queue <int > q;int val[N];void init () { for (int i=1 ;i<=n;i++) vec[i].clear(); while (!q.empty())q.pop(); memset (in,0 ,sizeof in); memset (val,0 ,sizeof val); } void topsort () { for (int i=1 ;i<=n;i++) if (in[i]==0 )q.push(i); int cnt=0 ; while (!q.empty()){ int p=q.front(); q.pop(); cnt++; for (int j=0 ;j<vec[p].size();j++){ int v=vec[p][j]; in[v]--; if (in[v]==0 )q.push(v),val[v]=val[p]+1 ; } } if (cnt!=n)cout <<-1 <<endl ; else { ll sum=0 ; for (int i=1 ;i<=n;i++)sum+=888 +val[i]; cout <<sum<<endl ; } } signed main () { ios::sync_with_stdio(false ),cin .tie(0 ); while (cin >>n>>m){ init(); int x,y; for (int i=1 ;i<=m;i++){ cin >>x>>y; vec[y].push_back(x); in[x]++; } topsort(); } return 0 ; }
经典例题 :不同值的升序排序序列是指使用某种形式的小于运算符将元素从最小到最大排序的序列。例如,排序序列 A , B , C , D 意味着 A < B , B < C , C < D 。在这个问题中,我们将给你一组 A < B 的关系,并要求你确定是否已经指定了排序顺序。
思路:参考讲解 ——此题难在要确定判断输出结果的依据,所以只能边存边排序,其次是对不同状态的判断方法。做法:边读边进行拓扑排序,如果最后拓扑的个数小于 n
说明图中有环返回 0 ,否则看能否条件是否拓扑唯一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <bits/stdc++.h> using namespace std ;int n,m,sign;int in[26 ],out[26 ];string s;vector <int > edge[26 ];queue <int > q;void init () { memset (in,0 ,sizeof in); for (int i=0 ;i<n;i++) edge[i].clear(); sign=0 ; } int topsort () { while (!q.empty())q.pop(); int cnt=0 ,flag=1 ,inc[26 ]; for (int i=0 ;i<n;i++) inc[i]=in[i]; for (int i=0 ;i<n;i++){ if (!inc[i])q.push(i); } while (!q.empty()){ if (q.size()>1 )flag=-1 ; int now=q.front(); out[cnt++]=now; q.pop() ; for (int i=0 ;i<edge[now].size();i++) if (--inc[edge[now][i]]==0 ) q.push(edge[now][i]); } if (cnt!=n)return 0 ; return flag; } signed main () { while (cin >>n>>m){ getchar(); if (n==0 &&m==0 )break ; init(); for (int i=1 ;i<=m;i++){ cin >>s; if (sign)continue ; int ch1=s[0 ]-'A' ,ch2=s[2 ]-'A' ; edge[ch1].push_back(ch2); in[ch2]++; int k=topsort(); if (k==0 ){ printf ("Inconsistency found after %d relations.\n" ,i); sign=1 ; } if (k==1 ){ printf ("Sorted sequence determined after %d relations: " ,i); for (int j=0 ;j<n;j++) printf ("%c" ,out[j]+'A' );printf (".\n" ); sign=1 ; } } if (!sign)printf ("Sorted sequence cannot be determined.\n" ); } return 0 ; }
强连通分量
参考博客 参考这篇博客以及队内资料多理解,只是绕但是不难
预备知识
强连通的定义 :如果图中节点间可以相互通达则称他们为强连通
强连通图定义 :如果有向图 G
的任意节点之间都强连通,称 G
是一个强连通图
强连通分量定义 :非强连通图有向图的极大强连通子图,称为强连通分量
注意:单个节点也看做是一个强连通图
例如:在这个有向图中 {1,2,3,4}
四个点可以互相到达,就称这四个点组成的子图为强连通分量(这四个点中任意两两节点强连通),当然 {5}、{6}
也是强连通分量
Tarjan算法求强连通分量
相关知识
关于 DFS 搜索树中的四条边:
树枝边 :DFS 时经过的边,即 DFS 搜索树上的边
前向边 :与 DFS 方向一致,从某节点指向其某个子孙的边
后向边 :与 DFS 方向相反,从某节点指向其某个祖先的边(又称返祖边)
横叉边 :从某个节点指向搜索树中的另一子树中的某节点的边
思想介绍
Tarjan算法是借助栈 来求强连通分量的算法,它是一种基于 DFS 的算法,每个强连通分量为搜索树中的一棵子树 。
在介绍详细原理前,先引入两个关于图中节点非常重要的数组:dfn[]
与 low[]
:
dfn[]
就是一个时间戳(被搜索到的次序)一旦某个点被DFS到后,这个时间戳就不再改变且每个节点拥有唯一的时间戳,所以所以常根据 dfn
的值来判断是否需要进行进一步的深搜
low[]
记录当前节点的子树中能访问到的仍在栈中的最小时间戳,像是确立了某个关系,low
相等的点在同一强连通分量中
初始化时 dfn[i]=low[i]=++cnt
具体做法
首先这个图不一定是连通图(有可能是分为几块),所以枚举每个点 Tarjan ,若 dfn[i]==0
则继续向下DFS( main()
中)
对于搜到的点先处理当前点的 dfn
和 low
并将该点入栈,然后看与其有边相连的点,判断这些点是否已经被搜索过,若没有则进行搜索。若点已经在栈中,说明形成了环,则更新当前搜索到的点的 low
( Tarjan()
中)
在不断 DFS 的过程中如果当前点没有路可走了(出边遍历完了),那么就进行回溯。回溯时不断更新之前节点的 low
,每个节点都取最小的 low
值。如果回溯完 x
节点时 dfn[x]==low[x]
则 x
可以看作是某一强连通分量子树的根,也说明找到了一个强连通分量,然后对栈进行弹出操作,直到 x
被弹出( x
和其后面的子节点构成了一个强连通分量)( Tarjan()
中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void Tarjan (int now) { dfn[now]=low[now]=++Index; stack [++tot]=now; vis[now]=1 ; for (int i=f[now];i!=-1 ;i=e[i].next){ int nex=e[i].v; if (!dfn[nex]){ Tarjan(nex); low[now]=min(low[now],low[nex]); } else if (vis[nex])low[now]=min(low[now],dfn[nex]); } if (dfn[now]==low[now]){ int p; do { p=stack [tot--]; vis[p]=0 ; cout <<p<<" " ; }while (p!=now); } cout <<endl ; }
算法演示
就以一个连通图从节点 1 开始 Tarjan 为例,注意这个图示的入栈顺序是由上到下
从节点1开始 DFS ,每搜索到一个节点就处理节点 dfn
和 low
(初始 dfn[i]=low[i]=++index
)并将节点放入栈中。当深搜到节点 6 时 dfn[6]==low[6]
所以找到了一个强连通分量,弹出栈内节点 6 和下面的节点 {6}
·
回溯到节点5后还是 dfn[5]==low[5]
所以又找到一个强连通分量,弹出栈内节点 5 和下面的节点 {5}
回溯到节点 3 后还有通向节点4的边所以搜索到节点 4 并把节点 4 放入堆栈。紧接着发现搜索节点4可以到祖辈节点 1(后向边),所以更新 low[4]=1
,还可以到节点 6 但节点 6 已经出栈(横叉边)所以回溯值节点 3 ,此时节点 3 被更新 low[3]=min(low[3],low[4])=1
( 3 , 4 为树状边)(第一种更新情况)
回溯到节点 1 ,节点 1 还有可以搜索到节点 2 并把节点 2 放入栈中。紧接着搜索节点 2 访问到还在栈中的节点 4 ,所以更新节点 2 为 low[2]=min(low[2],dfn[4])=5
(第二种更新情况)。回溯到节点 1 后发现 dfn[1]==low[1]
说明找到以节点 1 为根的强连通分量,弹出节点1及下面的节点 {1,3,4,2}
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const int maxn=3e5 +5 ;int n,m;int tot,Index,top;int dfn[maxn],low[maxn];int Stack[maxn];bool vis[maxn];int head[maxn];struct edge { int u,v,w; int nex; }e[maxn*maxn]; void init () { tot=Index=top=0 ; memset (vis,0 ,sizeof vis); memset (head,-1 ,sizeof head); memset (dfn,0 ,sizeof dfn); } void add (int u,int v,int w) { ++tot; e[tot].u=u; e[tot].v=v; e[tot].w=w; e[tot].nex=head[u]; head[u]=tot; } void Tarjan (int now) { dfn[now]=low[now]=++Index; Stack[++top]=now; vis[now]=1 ; for (int i=head[now];i!=-1 ;i=e[i].nex){ int nex=e[i].v; if (!dfn[nex]){ Tarjan(nex); low[now]=min(low[now],low[nex]); } else if (vis[nex])low[now]=min(low[now],dfn[nex]); } if (dfn[now]==low[now]){ int p; do { p=Stack[top--]; vis[p]=0 ; cout <<p<<" " ; }while (p!=now); cout <<endl ; } } signed main () { while (cin >>n>>m){ init(); for (int i=1 ;i<=m;i++){ int x,y; cin >>x>>y; add(x,y,0 ); } for (int i=1 ;i<=n;i++) if (dfn[i]==0 ){ Tarjan(i); cout <<endl ; } } return 0 ; }
模板题 :有 n
个点 m
个有向边组成的有向图,现在要在某些点上设置警察局,在第 i
个点建立警察局的花费是 a[i]
,并且 i
点能保护的点是其所在强连通分量的所有点。现在计划要用最少的花费建立警察局,让你求出最小的花费和可以选择的方案数(在不同的节点建立被认为是不同的方案,对 1e9+7 取模)
思想:用 ans1
记录最小消费,ans2
记录选择种数,最小消费就是各有向图中所有强连通分量的最小节点权值的加和,选择种数根据乘法原理就是各强连通分量中最小权值节点个数的乘积
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <bits/stdc++.h> using namespace std ;typedef long long ll;const int maxn=3e5 +5 ;const int inf=0x3f3f3f3f ;const ll mod=1e9 +7 ;int n,m;int tot,Index,top;ll ans1,ans2; int a[maxn];int dfn[maxn],low[maxn];int Stack[maxn];bool vis[maxn];int head[maxn];struct edge { int u,v,w; int nex; }e[maxn]; void init () { tot=Index=top=0 ; ans1=0 ,ans2=1 ; memset (vis,0 ,sizeof vis); memset (head,-1 ,sizeof head); memset (dfn,0 ,sizeof dfn); } void add (int u,int v,int w) { ++tot; e[tot].u=u; e[tot].v=v; e[tot].w=w; e[tot].nex=head[u]; head[u]=tot; } void Tarjan (int now) { dfn[now]=low[now]=++Index; Stack[++top]=now; vis[now]=1 ; for (int i=head[now];i!=-1 ;i=e[i].nex){ int nex=e[i].v; if (!dfn[nex]){ Tarjan(nex); low[now]=min(low[now],low[nex]); } else if (vis[nex])low[now]=min(low[now],dfn[nex]); } if (dfn[now]==low[now]){ int p; int minn=inf; int sum=0 ; do { p=Stack[top--]; vis[p]=0 ; if (a[p]==minn)sum++; else if (a[p]<minn) sum=1 ,minn=a[p]; }while (p!=now); ans1+=minn; ans2=(ans2*sum)%mod; } } signed main () { while (cin >>n){ init(); for (int i=1 ;i<=n;i++)cin >>a[i]; cin >>m; for (int i=1 ;i<=m;i++){ int x,y; cin >>x>>y; add(x,y,0 ); } for (int i=1 ;i<=n;i++) if (dfn[i]==0 )Tarjan(i); cout <<ans1<<" " <<ans2<<endl ; } return 0 ; }