Tarjan求点双连通分量
概述
在一个无向图中,若任意两点间至少存在两条“点不重复”的路径,则说这个图是点双连通的(简称双连通,biconnected)
在一个无向图中,点双连通的极大子图称为点双连通分量(简称双连通分量,Biconnected Component,BCC)
性质
- 任意两点间至少存在两条点不重复的路径等价于图中删去任意一个点都不会改变图的连通性,即BCC中无割点
- 若BCC间有公共点,则公共点为原图的割点
- 无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC
算法
在Tarjan过程中维护一个栈,每次Tarjan到一个结点就将该结点入栈,回溯时若目标结点low值不小于当前结点dfn值就出栈直到目标结点(目标结点也出栈),将出栈结点和当前结点存入BCC
(说实话我觉得存点不比存边难理解和实现啊……下面会解释)
理解
首先申明一下,在我找到的BCC资料中,在算法实现中均将两个点和一条边构成的图称为BCC,此文章也沿用此的规定
如下图:
我猜想可能是因为割点的定义,此图中两个点均不为割点,所以此图也属于BCC?
总之做题时注意题面要求,若要求的不含此种BCC则判断每个BCC的大小即可
无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC
有了上面的规定我们也不难理解这一条了:割点就算相邻也会属于至少两个BCC;BCC间的交点都是割点,所以非割点只属于一个BCC
到一个结点就将该结点入栈
为什么用栈存储呢?因为DFS是由上到下的,而分离BCC是自下而上的,需要后进先出的数据结构——栈
回溯时若目标结点low值不小于当前结点dfn值就出栈直到目标结点(目标结点也出栈),将出栈结点和当前结点存入BCC
对于每个BCC,它在DFS树中最先被发现的点一定是割点或DFS树的树根
证明:割点是BCC间的交点,故割点在BCC的边缘,且BCC间通过割点连接,所以BCC在DFS树中最先被发现的点是割点;特殊情况是对于开始DFS的点属于的BCC,其最先被发现的点就是DFS树的树根
上面的结论等价于每个BCC都在其最先被发现的点(一个割点或树根)的子树中
这样每发现一个BCC(low[v]>=dfn[u]),就将该子树出栈,并将该子树和当前结点(割点或树根)加入BCC中。上面的操作与此描述等价
(我就是因为这个条件“将子树出栈”没理解写错了结果调了一晚上poj2942)
综上,存点是不是很好理解?存边虽然不会涉及重复问题(割点属于至少两个BCC),但会有很多无用操作。个人觉得存点也是个不错的选择。
模板
并没有模板题
1 #include<cstdio> 2 #include<cctype> 3 #include<vector> 4 using namespace std; 5 struct edge 6 { 7 int to,pre; 8 }edges[1000001]; 9 int head[1000001],dfn[1000001],dfs_clock,tot; 10 int num;//BCC数量 11 int stack[1000001],top;//栈 12 vector<int>bcc[1000001]; 13 int tarjan(int u,int fa) 14 { 15 int lowu=dfn[u]=++dfs_clock; 16 for(int i=head[u];i;i=edges[i].pre) 17 if(!dfn[edges[i].to]) 18 { 19 stack[++top]=edges[i].to;//搜索到的点入栈 20 int lowv=tarjan(edges[i].to,u); 21 lowu=min(lowu,lowv); 22 if(lowv>=dfn[u])//是割点或根 23 { 24 num++; 25 while(stack[top]!=edges[i].to)//将点出栈直到目标点 26 bcc[num].push_back(stack[top--]); 27 bcc[num].push_back(stack[top--]);//目标点出栈 28 bcc[num].push_back(u);//不要忘了将当前点存入bcc 29 } 30 } 31 else if(edges[i].to!=fa) 32 lowu=min(lowu,dfn[edges[i].to]); 33 return lowu; 34 } 35 void add(int x,int y)//邻接表存边 36 { 37 edges[++tot].to=y; 38 edges[tot].pre=head[x]; 39 head[x]=tot; 40 } 41 int main() 42 { 43 int n,m; 44 scanf("%d%d",&n,&m); 45 for(int i=1;i<=m;i++) 46 { 47 int x,y; 48 scanf("%d%d",&x,&y); 49 add(x,y),add(y,x); 50 } 51 for(int i=1;i<=n;i++)//遍历n个点tarjan 52 if(!dfn[i]) 53 { 54 stack[top=1]=i; 55 tarjan(i,i); 56 } 57 for(int i=1;i<=num;i++) 58 { 59 printf("BCC#%d: ",i); 60 for(int j=0;j<bcc[i].size();j++) 61 printf("%d ",bcc[i][j]); 62 printf("\n"); 63 } 64 return 0; 65 }
双连通分量