[Tarjan系列] Tarjan算法求无向图的双连通分量
这篇介绍如何用Tarjan算法求Double Connected Component,即双连通分量。
双联通分量包括点双连通分量v-DCC和边连通分量e-DCC。
若一张无向连通图不存在割点,则称它为“点双连通图”,不存在桥则称为“边双连通图”。
无向图的极大点双连通子图就v-DCC,极大边双连通子图就是e-DCC。
上一篇我们讲了如何用Tarjan算法求出无向图中的所有割点和桥。
不会求的朋友们可以去看一看上篇文章:Tarjan算法求无向图的割点和桥
这里“极大”的定义可以理解为包含部分点的最大的双连通子图,即不存在比包含它且比它更大的双连通子图。
下面给出几个定理:
1. 一张无向连通图是点双连通图当且仅当 图的顶点数<=2 or 图中任意两点都同时包含在至少一个简单环中。
2. 一张无向连通图是边双连通图当且仅当任意一条边都包含在至少一个简单换中。
接下来讲求法:
e-DCC的求法很简单,通过一遍Tarjan算法找到所有的桥,把桥删除后,无向图会分裂成一个个连通块。
每一个连通块都是一个e-DCC。
具体实现就是先用Tarjan算法标记所有桥,然后对整张图dfs一遍(不访问桥边),划分出所有连通块。
一般可以用一个数组表示每个节点所在的e-DCC的编号。
代码如下:
#include<bits/stdc++.h> #define N 100010 using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } struct Edge{ int nxt,to; #define nxt(x) e[x].nxt #define to(x) e[x].to }e[N<<1]; int head[N],tot=1,n,m,cnt,dfn[N],low[N],c[N],bridge[N],dcc; //c[x]储存x所在的e-DCC的编号,dcc存e-DCC的数量 inline void addedge(int f,int t){ nxt(++tot)=head[f];to(tot)=t;head[f]=tot; } void tarjan(int x,int in_edge){ dfn[x]=low[x]=++cnt; for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(!dfn[y]){ tarjan(y,i); low[x]=min(low[x],low[y]); if(low[y]>dfn[x]) bridge[i]=bridge[i^1]=1; }else if(i!=(in_edge^1)) low[x]=min(low[x],dfn[y]); } }//Tarjan标记桥 void dfs(int x){ c[x]=dcc;//标号 for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(c[y]||bridge[i])continue;//如果已经有标号了或者这条边是桥就不访问 dfs(y); } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); addedge(x,y);addedge(y,x); } for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i,0); for(int i=1;i<=n;i++){ if(!c[i]){ ++dcc;dfs(i);//每个联通块都进去标号 } } for(int i=1;i<=n;i++) printf("%d %d\n",i,c[i]); return 0; }
接下来讲v-DCC的求法。
v-DCC是一个很容易混淆的概念。
由于v-DCC定义中的“极大”,一个割点可能属于多个v-DCC。
为了求出v-DCC,我们需要在Tarjan的过程中维护一个栈。
当一个点第一次被访问时,我们将它入栈。而当割点判定法则成立时,无论x是否为根,都要
从栈顶不断弹出节点直到y节点被弹出,这些被弹出的节点包括x节点一起构成一个v-DCC。
听上去挺简单的,实际上代码也很好写。
#include<bits/stdc++.h> #define N 100010 using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } struct Edge{ int nxt,to; #define nxt(x) e[x].nxt #define to(x) e[x].to }e[N<<1]; int head[N],tot=1,n,m,rt,dfn[N],low[N],cnt,stk[N],top,num,cut[N]; vector<int> dcc[N]; inline void addedge(int f,int t){ nxt(++tot)=head[f];to(tot)=t;head[f]=tot; } void tarjan(int x){ dfn[x]=low[x]=++cnt; stk[++top]=x;//第一次访问该节点,入栈 if(x==rt && head[x]==0){//判断孤立点,直接插入vector dcc[++num].push_back(x); return; } int flag=0; for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(!dfn[y]){ tarjan(y); low[x]=min(low[x],low[y]); if(low[y]>=dfn[x]){//割点判定法则 flag++; if(x!=rt||flag>1)cut[x]=1; num++;int z;//根据上面描述的做法,把所有栈中的节点插入vector do{ z=stk[top--]; dcc[num].push_back(z);//全部插入 }while(z!=y); dcc[num].push_back(x);//包括x自己 } }else low[x]=min(low[x],dfn[y]);//不在搜索树上,继续更新low } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); addedge(x,y);addedge(y,x); } for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i); for(int i=1;i<=num;i++){ printf("%d:",i);//第i个v-DCC for(int j=0;j<dcc[i].size();j++) printf(" %d",dcc[i][j]);//第i个v-DCC中所有的点 putchar('\n'); } return 0; }
那么这一篇就讲到这里了,下一篇写一篇短博客更新e-DCC和v-DCC的缩点。