一、引言
强连通分量是指有向图的一个极大联通子图,强连通分量中任意两个点都存在一条路径可以直接或间接互相到达。特别地,有向图G中,若对于 V(G) 中任意两个不同的顶点 u 和 v,都存在从 u 到 v 以及从 v 到 u 的路径,则称 G 是强连通图。
有向图的极大强连通子图被称为是“强连通分量”,简记为SCC。
举个栗子。如右图,G1是强连通的,G2不是强连通的。因为G2中,点4不能到达点1。
求强连通分量的Tarjan算法,时间复杂度可以做到 O(n+m)。
二、强连通分量缩点
若将有向图中的强连通分量都缩成一个点,则原图会形成一个DAG(有向无环图)。举个栗子:
三、基本算法
一些定义
Tarjan算法是基于深搜的算法,每个强联通分量为搜索树种的一棵子树。搜索时,把当前搜索树中未处理的结点加入堆栈,回溯时就可以判断栈顶到栈中的结点是否为一个强联通分量。
为了描述方便,我们对图的边进行一些定义。一棵 DFS 树被构造出来后,考虑图中的非树边,对边定义如下:
前向边:祖先→儿子的边。
后向边:儿子→祖先的边。
横叉边:没有祖先、儿子关系的边(注意横叉边只会往 dfn 减小的方向链接。)
dfn[x]:记录结点x在DFS过程中被遍历到的次序号(时间戳)。可知在同一个DFS树的子树中,dfn[x]越小,则其越浅。
low[x]:记录结点x或x的子树能够追溯到的dfn最小的值(栈中标号的最小点)。即在DFS树中,此点以及其后代指出去的边,能返回到的最浅的点的时间戳。
low的计算过程
我们阔以模拟一下low的计算过程。
举个栗子。
dfn[x]为结点x在DFS过程中被遍历到的次序号,不用解释。low[x]为结点x或x的子树能够追溯到的dfn最小的值(在DFS树中,此点以及其后代指出去的边,能返回到的最浅的点的时间戳)。
从结点1开始遍历。当到搜索结点5的时候,结点5已经没有指出去的边了,就是说,它不能再指向更浅的时间戳了。它就指向自己,所以结点5的low值就是4。接下来搜索4号点、6号点。搜索到6号点后,6号点返回,找到了4号点。所以6号点能搜索到的最浅的时间戳就是5(即4号点的dfn值)。4号点不能往其他地方遍历了,所以它的low值就是5。同理,可推出其他结点的low值。
可得,当dfn[x]=low[x]时,以x为根的搜索子树上所有结点构成一个强联通分量。
证明:dfn表示x点被DFS到的时间,low表示x和x所以子树结点最多只有指向x点的边,而没有指向x的祖先的边了。显然,遍历过的结点从x出发又最终回到x形成了一个环,即x点与它的子孙结点构成了强联通分量。
四、代码实现
根据定义,Tarjan算法按照以下步骤计算“追溯值”:
1.当节点x第一次被访问时,把x入栈,初始化 low[x]=dfn[x]。
2.扫描从x出发的每条边(x,y)。
- 若y没被访问过,则说明(x,y)是树枝边,递归访问y,从y回溯之后,令 low[x]=min(low[x],low[y])。
- 若y被访问过并且y在栈中,则令 low[xmin(low[x],dfn[y])。
3.从x回溯之前,判断是否有 low[x]=dfn[x]。若成立,则不断从栈中弹出节点,直至x出栈。
(这个板子好奇怪,感觉会炸的亚子)
缩点后重建图:上述代码中,ins[x]表示x这个点所属的SCC编号(我也不知道为什么要用这个变量名)。那么建图的话,只需要遍历原有的边,如果边所对应的两个节点所属的SCC编号不同,那么则添边,最后得到一个有向无环图。
五、例题
[HAOI2006]受欢迎的牛
每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果 A 喜欢 B,B 喜欢 C,那么 A 也喜欢 C。牛栏里共有 N 头奶牛,给定一些奶牛之间的爱慕关系,请你算出有多少头奶牛可以当明星。
首先考虑求出所有的极大强连通分量,显然一个分量内的牛是相互“受欢迎”的。考虑将每个分量看作一个点,以原图中端点属于不同分量的点为边组成新的图G,那么显然G中是没有环的(否则与极大的条件矛盾),这样图必定存在至少一个点出度为0,如果有多个点出度为0,那么答案显然为0(这些点之间显然无法直接或间接存在“受欢迎”关系)。如果只有一个点出度为0,那么只要满足所有点都能到达它即可,否则答案为0。如果上述条件均满足,那么答案就是该点对应的原分量中的点数。
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 const int N=1e4+5,M=5e4+5; 5 int n,m,cnt,num,dfn[N],low[N],ins[N],st[N],top,x,y,v[N],ans,k[N]; 6 int hd[N],to[M],nxt[M]; 7 void add(int x,int y){ 8 to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt; 9 } 10 void tarjan(int x){ 11 dfn[x]=low[x]=++num,st[++top]=x; 12 for(int i=hd[x];i;i=nxt[i]){ 13 int y=to[i]; 14 if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]); 15 else if(!ins[y]) low[x]=min(low[x],dfn[y]); 16 } 17 if(low[x]==dfn[x]){ 18 ins[x]=++cnt,++k[cnt]; 19 while(st[top]!=x) ++k[cnt],ins[st[top]]=cnt,--top; 20 --top; 21 } 22 } 23 signed main(){ 24 //freopen(".in","r",stdin); 25 //freopen(".out","w",stdout); 26 scanf("%lld%lld",&n,&m); 27 for(int i=1;i<=m;i++){ 28 scanf("%lld%lld",&x,&y); 29 add(y,x); 30 } 31 cnt=0,x=0; 32 for(int i=1;i<=n;i++) 33 if(!dfn[i]) tarjan(i); 34 for(int i=1;i<=n;i++) 35 for(int j=hd[i];j;j=nxt[j]) 36 if(ins[i]!=ins[to[j]]) v[ins[to[j]]]++; 37 for(int i=1;i<=cnt;i++) 38 if(!v[i]) ans=k[i],x++; 39 if(x==1) printf("%lld\n",ans); 40 else puts("0"); 41 return 0; 42 }
POJ1236 Network of Schools
题目大意:给定一个有向图,N个点,求:
(1) 至少要选几个顶点, 才能做到从这些顶点出发, 可以到达全部顶点。
(2) 至少要加多少条边,才能使得从任何一个顶点出发,都能到达全部项点。
Tarjan算法求SCC,并缩点建图。
那么对于问题1,新的图中入度为0的点的点数即是答案。
对于问题2,答案为max (入度为0的点的点数,出度为0的点数),因为对于每个入度或出度为0的点,需要连一条边来解决,那么将出度为0的点连向入度为0的点是最优的。
此外,如果最后SCC只有1个,那么问题2的答案应该特判为0。
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 signed main(){ 5 //freopen(".in","r",stdin); 6 //freopen(".out","w",stdout); 7 puts("Dlstxdy,lkytsdy!"); 8 return 0; 9 }
显然这个代码是过不了这道题的
(待更新,先占个坑欢迎纠错)
原文地址:https://www.cnblogs.com/maoyiting/p/12592849.html