Tarjan三大算法之双连通分量(双连通分量)

https://blog.csdn.net/fuyukai/article/details/51303292

定义: 

对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为 点双连通的(简称双连通);如果任意两点至少存在两条边不重复路径,则称该图为 边双连通的。点双连通图的定义等价于任意两条边都同在一个简单环中,而边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,点双连通的极大子图称为 点双连通分量(简称双连通分量),边双连通的极大子图称为 边双连通分量。这篇博客就是总结一下求解无向图点双连通分量与边双连通分量的方法。

算法: 
求解点双连通分量与边双连通分量其实和求解割点与桥密切相关。不同双连通分量最多只有一个公共点,即某一个割顶,任意一个割顶都是至少两个点双连通的公共点。不同边双连通分量没有公共点,而桥不在任何一个边双连通分量中,点双连通分量一定是一个边双连通分量。 
下面首先介绍点双连通分量的Tarjan算法: 
在之前的博客中,我们已经知道如何求解割顶了,很容易可以发现,当我们找到割顶的时候,就已经完成了一次对某个极大点双连通子图的访问,那么我们如果在进行DFS的过程中将遍历过的点保存起来,是不是就可以得到点双连通分量了?为了实现算法,我们可以在求解割顶的过程中用一个栈保存遍历过的(注意不是点!因为不同的双连通分量存在公共点即割顶),之后每当找到一个点双连通分量,即子结点v与父节点u满足关系low[v]>=dfn[u],我们就将栈里的东西拿出来直到遇到当前边。 
这里注意放入栈中的不是点,而是边,这是因为点双连通分量是存在重复点的,如果我们放入栈中的是点,那么对于某些点双连通分量,就会少掉一些点(这些点都是割顶)。 
代码:

struct Edge{
    int u,v;
    Edge(int u=0,int v=0):u(u),v(v){}
}e[maxm];
int n,m,stamp,dfn[maxn],low[maxn],iscut[maxn],bccno[maxn];
int scnt,stack[maxm],bcc_cnt;
vector<int> vec[maxn],bcc[maxn];

void tarjan(int index,int fa)
{
    int child=0,tmp;
    dfn[index]=low[index]=++stamp;
    for(int i=0;iif(!dfn[tmp])
        {
            stack[++scnt]=vec[index][i],child++;
            tarjan(tmp,index);
            low[index]=min(low[index],low[tmp]);
            if(low[tmp]>=dfn[index])
            {
                iscut[index]=1;
                bcc[++bcc_cnt].clear();
                while(1)
                {
                    int num=stack[scnt--];
                    if(bccno[e[num].u]!=bcc_cnt)
                    {
                        bcc[bcc_cnt].push_back(e[num].u);
                        bccno[e[num].u]=bcc_cnt;
                    }
                    if(bccno[e[num].v]!=bcc_cnt)
                    {
                        bcc[bcc_cnt].push_back(e[num].v);
                        bccno[e[num].v]=bcc_cnt;
                    }
                    if(e[num].u==index && e[num].v==tmp)
                        break;
                }
            }
        }
        else if(dfn[tmp]stack[++scnt]=vec[index][i];
            low[index]=min(low[index], dfn[tmp]);
        }
    }
    if(fa<0 && child==1)
        iscut[index]=0;
}

void find_bcc()
{
    // 割顶的bccno值无意义 
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(iscut,0,sizeof(iscut));
    memset(bccno,0,sizeof(bccno));
    memset(bcc,0,sizeof(bcc));
    stamp=scnt=bcc_cnt=0;
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i,-1);
}

这里需要十分注意的是,算法结束之后,每个结点会有一个编号,代表它属于哪一个点双连通分量,但是,割顶的编号是完全没有意义的!这个算法灵活使用了两个时间戳和栈,完成了点双连通分量的发现。 
例题:UVALIVE 5135

之后介绍边双连通分量的求解算法: 
边双连通分量的求解非常简单,因为边双连通分量之间没有公共边,而且桥不在任意一个边双连通分量中,所以算法十分简单,即先一次DFS找到所有桥,再一次DFS(排除了桥)找到边双连通分量。 
PS:当然可以用一次DFS实现。 
代码:

struct Edge{
    int u,v;
    Edge(int u=0,int v=0):u(u),v(v){}
}e[maxm];
int n,m,stamp,dfn[maxn],low[maxn],bccno[maxn],bcc_cnt;
vector<int> vec[maxn],bcc[maxn];
bool g[maxn][maxn],isbridge[maxm];

void tarjan(int index,int fa)
{
    int tmp;
    dfn[index]=low[index]=++stamp;
    for(int i=0;iif(!dfn[tmp])
        {
            tarjan(tmp,index);
            low[index]=min(low[index],low[tmp]);
            if(low[tmp]>dfn[index])
                isbridge[vec[index][i]]=isbridge[vec[index][i]^1]=1;
        }
        else if(dfn[tmp]void dfs(int index)
{
    dfn[index]=1;
    bccno[index]=bcc_cnt;
    for(int i=0;iint tmp=vec[index][i];
        if(isbridge[tmp])
            continue;
        if(!dfn[e[tmp].v])
        {
            dfs(e[tmp].v);
        }
    }
}

void find_ebcc(){
    bcc_cnt=stamp=0;
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(isbridge,0,sizeof(isbridge));
    memset(bccno,0,sizeof(bccno));
    memset(bcc,0,sizeof(bcc));
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i, -1);
    memset(dfn,0,sizeof(dfn));
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            bcc_cnt++;
            dfs(i);
        }
    }               
}

例题:POJ 3352


Codeforces 962F Simple Cycles Edges [Tarjan+点双连通分量]

题意:给你一张无自环,无重边的无向图,定义简单环是环中的点只出现一次,求所有的只包含在一个简单环中的边。

题解:由于只能包含在一个简单环中,我们可以考虑tarjan缩点,然后我们对于以下这张图。


根据题意我们可以得到答案是6条边,因此我们需要通过点双连通分量缩点,而不是边双连通分量缩点。之后我们只要计算每一个点双连通分量中,点的数量与边的数量是否相同,就可以判断这个分量中的边是否满足条件。

点双连通分量内容,请移至博客:https://blog.csdn.net/acterminate/article/details/52526920

AC代码:

[cpp]  view plain  copy
  1. #include  
  2. #include  
  3. #include  
  4. #include  
  5. #include  
  6. #define N 100005  
  7. #define M 200005  
  8. using namespace std;  
  9. struct edge  
  10. {  
  11.     int to,next;  
  12.     edge(){}  
  13.     edge(int to,int next)  
  14.     {  
  15.         this->to=to;  
  16.         this->next=next;  
  17.     }  
  18. }ed[M];  
  19. vector<int>vt[N];  
  20. int head[N],lnum,esum,index,top,bccnum;  
  21. int dfn[N],low[N],mark[M],bj[N],st[M],belong[M],isCut[N],num[N];  
  22. pair<int,int>E[M];  
  23. void addline(int from,int to)  
  24. {  
  25.     ed[lnum]=edge(to,head[from]);  
  26.     head[from]=lnum++;  
  27. }  
  28. void tarjan(int root,int fa){//有自环时不加自环的边  
  29.                             //点双连通缩点方法:清空路径,枚举E[]数组中存储的路径,建立双向边。  
  30.     dfn[root]=low[root]=++index; //新点初始化  
  31.     int child=0;  
  32. //初始节点需要两个以上儿子且dfn[root]<=low[v] 才是割点  
  33.     for(int i=head[root];~i;i=ed[i].next){ //遍历root指出去的边  
  34.         int v=ed[i].to;  
  35.         if(mark[i]) continue;  
  36.         mark[i]=mark[i^1]=1;  
  37.         st[++top]=i;//边入栈,需注意此语句要放在判continue之后  
  38.          if(!dfn[v]){ //如果v节点未去过,搜索v节点  
  39.             child++;  
  40.             tarjan(v,root);  
  41.             low[root]=min(low[root],low[v]); //更新low值  
  42.             if(dfn[root]<=low[v]){  
  43.                 isCut[root]=1;          //此点是割点,需注意初始节点要有两个儿子  
  44.                 bccnum++;//注意这里是N++,建数组时要注意开至少两倍大  
  45.                 for(;;){  
  46.                     int j=st[top--];  
  47.             //bj[]数组用来标记节点所属的bcc,割点会改变,无意义  
  48.             //E[]存新图的边,esum是其数量,tarjan结束后建双向边  
  49.                     if(bj[ed[j].to]!=bccnum){  
  50.                         bj[ed[j].to]=bccnum;  
  51.                         num[bccnum]++;  
  52.                         E[++esum]=make_pair(ed[j].to,bccnum);  
  53.                     }  
  54.                     if(bj[ed[j^1].to]!=bccnum){  
  55.                         bj[ed[j^1].to]=bccnum;  
  56.                         num[bccnum]++;  
  57.                         E[++esum]=make_pair(ed[j^1].to,bccnum);  
  58.                     }  
  59.                     belong[(j>>1)+1]=bccnum;//标记边所属的bcc  
  60.                     if(i==j)break;  
  61.                 }  
  62.             }  
  63.         }  
  64.         else low[root]=min(low[root],dfn[v]);  
  65.     //与有向图区分,此处else不需要判别v节点是否在栈内  
  66.      }  
  67.     if(root==fa && child<2)isCut[root]=0;  
  68.     //如果初始节点没有2个以上儿子,标记清0  
  69. }  
  70.   
  71. void init()  
  72. {  
  73.     memset(head,-1,sizeof(head));  
  74.     memset(dfn,0,sizeof(dfn));  
  75.     memset(low,0,sizeof(low));  
  76.     memset(mark,0,sizeof(mark));  
  77.     memset(belong,0,sizeof(belong));  
  78.     memset(isCut,0,sizeof(isCut));  
  79.     top=0;  
  80.     lnum=0;  
  81.     index=0;  
  82.     bccnum=0;  
  83.     esum=0;  
  84. }  
  85. vector<int>ans;  
  86. int main()  
  87. {  
  88.     init();  
  89.     int n,m;  
  90.     scanf("%d%d",&n,&m);  
  91.     for(int i=1;i<=m;i++)  
  92.     {  
  93.         int u,v;  
  94.         scanf("%d%d",&u,&v);  
  95.         addline(u,v);  
  96.         addline(v,u);  
  97.     }  
  98.     for(int i=1;i<=n;i++)  
  99.         if(!dfn[i])  
  100.             tarjan(i,i);  
  101.     for(int i=1;i<=m;i++)  
  102.         vt[belong[i]].push_back(i);  
  103.     for(int i=1;i<=bccnum;i++)  
  104.         if(vt[i].size()==num[i])  
  105.             for(int j=0;j
  106.                 ans.push_back(vt[i][j]);  
  107.     sort(ans.begin(),ans.end());  
  108.     printf("%d\n",ans.size());  
  109.     for(int i=0;i
  110.         printf("%d ",ans[i]);  
  111.     printf("\n");  
  112. }  


你可能感兴趣的:(————ACM相关————,——图论相关——,ACM,-,割点与割边)