Tarjan-割点&桥&双连通
$Tarjan$求割点
感觉图论是个好神奇的东西啊,有各种奇奇怪怪的算法,而且非常巧妙。
周末之前说好回来之后进行一下学术交流,于是wzx就教了$Tarjan$,在这里我一定要说:
wzx AK IOI!
Tarjan发明了很多算法,而且还都叫一个名字,所以说只好用用途来区分它们。
闲聊时间结束。
首先,什么是割点呢?在一个无向图中,如果有一个顶点,删除这个顶点以及所有相关联的边以后,图的连通分量增多,就称这个点为割点。
首先找一个点作为根进行搜索,把图按照$dfs$的方法组织成一棵搜索树,树上的边一定都是图上的边,称为树边,而图上其余的边则为非树边(回边)。
如果一个点不能通过非树边而回到比他树上的父亲的$dfs$序更小的点,那么如果把它树上的父亲删掉,它就不能通过其他方法与图的其他部分联通,它的父亲就是一个割点。多么神奇啊!对于根节点,我们可以发现,如果它有不止一个的子树,那它就是割点了。看代码:
割点:https://www.luogu.org/problemnew/show/P3388
1 void dfs(int x,int roo,int Dad) 2 { 3 id[x]=low[x]=++cnt; 4 int j,cnts=0; 5 for (R i=firs[x];i;i=g[i].nex) 6 { 7 j=g[i].too; 8 if(!id[j]) 9 { 10 dfs(j,roo,x); 11 low[x]=min(low[x],low[j]); 12 if(x==roo) cnts++; 13 if(low[j]>=id[x]&&x!=roo) f[x]=1; 14 } 15 else 16 { 17 if(j!=Dad) low[x]=min(low[x],low[j]); 18 } 19 } 20 if(x==roo&&cnts>=2) f[x]=1; 21 }
这里有一句话还是比较重要的:
1 if(j!=Dad) low[x]=min(low[x],low[j]);
是防止重复走树边,其实也可以改成 $low[x]=min(low[x],id[j])$,这样更新出来的$low$可能不是真正的$low$,但是因为儿子到父亲的路径上不会再有别的点,所以这样也能保证正确性。
割点的理论知识似乎就到此为止了,现在还是看几道题比较好。
[POI2008]BLO-Blockade:https://www.luogu.org/problemnew/show/P3469
题意概述:在一个无向图中删去一个点后,还有多少个有序的(x,y)的点对本可以联通但是现在不能联通了?输出删除每一个点之后的答案。
这道题还是挺妙的,不算是板子题,有一点点思维含量,这样的题最适合入门新算法啦。
如果一个点不是割点,那么损失的点对只有与它直接相连的点;如果一个点是割点,那么割掉它之后图就分成了一些小块,在每个小块内仍是$size*(size-1)$;这么一说感觉也没啥思维难度啊...
1 # include <cstdio> 2 # include <iostream> 3 # define R register int 4 5 using namespace std; 6 7 const int maxn=100009; 8 const int maxm=500009; 9 int cnt=0,h=0,n,m,x,y,firs[maxn],f[maxn],id[maxn],low[maxn]; 10 long long ans[maxn],siz[maxn]; 11 struct edge 12 { 13 int too,nex; 14 }g[maxm<<1]; 15 16 void add(int x,int y) 17 { 18 g[++h].too=y; 19 g[h].nex=firs[x]; 20 firs[x]=h; 21 } 22 23 int read() 24 { 25 int x=0; 26 char c=getchar(); 27 while (!isdigit(c)) 28 c=getchar(); 29 while (isdigit(c)) 30 { 31 x=(x<<3)+(x<<1)+(c^48); 32 c=getchar(); 33 } 34 return x; 35 } 36 37 void dfs(int x,int roo) 38 { 39 id[x]=low[x]=++cnt; 40 int j,s=0; 41 siz[x]=1; 42 for (R i=firs[x];i;i=g[i].nex) 43 { 44 j=g[i].too; 45 if(!id[j]) 46 { 47 dfs(j,roo); 48 low[x]=min(low[x],low[j]); 49 siz[x]+=siz[j]; 50 if(low[j]>=id[x]) 51 { 52 ans[x]+=(long long)s*siz[j]; 53 s+=siz[j]; 54 } 55 } 56 else 57 low[x]=min(low[x],id[j]); 58 } 59 ans[x]+=(long long)s*(n-s-1); 60 } 61 62 int main() 63 { 64 n=read(); 65 m=read(); 66 for (R i=1;i<=m;++i) 67 { 68 x=read(); 69 y=read(); 70 add(x,y); 71 add(y,x); 72 } 73 for (R i=1;i<=n;++i) 74 if(!id[i]) dfs(i,i); 75 for (R i=1;i<=n;++i) 76 printf("%lld\n",(ans[i]+n-1)<<1); 77 return 0; 78 }
其实割点的题比较少见唉...
$Tarjan$求割边(桥)
什么是割边呢?如果删去一条边后整个图变得不连通了,那么这条边就叫做这个图的一个割边。显然割边是对于无向图的一种东西,有向图是无法定义图的连通性的。
割点和割边听起来总是有种神奇的联系,所以有两个猜想:
1.两个割点中间连一条边一定是桥;
2.桥的两个端点都是割点;
然而并不是...都是错的...
如果对图上的边对于在搜索树上出现的位置进行分类,可以分为:树枝边(搜索树上的树枝),返祖边(指向搜索树上祖先的边),正向边(指向后代,但不是搜索树上的边),横叉边(指向搜索树与它不在同一子树上的边).然而无向图不存在横叉边.考虑反证法:如果$X$有指向其他子树的边,那必然也有从那边指过来的边,所以之前搜索时应该已经搜到过$X$了,这是不成立的.
那么一条边成为桥需要什么条件呢?首先对于搜索树上的一个节点$x$和它的子节点$y$,如果$low[y]>id[x]$,也就是说如果不走$(x,y)$这条边,$y$就无法与搜索树的其他部分相连通,那么$(x,y)$这条边就是一条割边.但是可以发现$low[y]<=id[y]$且$low[y]>id[x]$,而且$x$是$y$的直接祖先,所以可以认为是$low[y]=id[y]$,这就和缩点有一些相似之处了.这里有一些细节问题需要注意:怎样防止走反向边回到父亲去?一种最简单的方法是记录每个点在搜索书上的父亲.很可惜,这样做是错误的.如果一个点和它的父节点间有重边,那么这几条边肯定都不是割边,但是这样的做法却可能误判成割边.正确的做法是记录上一条走的边的编号且利用网络流反向边的标号技巧,注意不要走它即可。
来一道例题吧:
旅游航道:https://loj.ac/problem/10102
题意概述:求割边的数量。
1 # include <cstdio> 2 # include <iostream> 3 # include <cstring> 4 # define R register int 5 6 using namespace std; 7 8 const int maxn=30004; 9 int n,m,h,firs[maxn],low[maxn],id[maxn],cnt,x,y,ed; 10 struct edge 11 { 12 int too,nex; 13 }g[maxn<<1]; 14 15 void dfs (int x,int las) 16 { 17 int j; 18 low[x]=id[x]=++cnt; 19 for (R i=firs[x];i;i=g[i].nex) 20 { 21 j=g[i].too; 22 if(i==(las^1)) continue; 23 if(!id[j]) dfs(j,i); 24 low[x]=min(low[x],low[j]); 25 if(low[j]>id[x]) ed++; 26 } 27 } 28 29 void add (int x,int y) 30 { 31 g[++h].nex=firs[x]; 32 firs[x]=h; 33 g[h].too=y; 34 } 35 36 int main() 37 { 38 scanf("%d%d",&n,&m); 39 while(n||m) 40 { 41 h=1; 42 cnt=ed=0; 43 memset(firs,0,sizeof(firs)); 44 memset(id,0,sizeof(id)); 45 memset(low,0,sizeof(low)); 46 for (R i=1;i<=m;++i) 47 { 48 scanf("%d%d",&x,&y); 49 add(x,y); 50 add(y,x); 51 } 52 for (R i=1;i<=n;++i) 53 if(!id[i]) dfs(i,-1); 54 printf("%d\n",ed); 55 scanf("%d%d",&n,&m); 56 } 57 return 0; 58 }
割边好像不是很难。但是,难的还在后面。
边双连通分量
边双连通分量是什么?
---shzr