zoukankan      html  css  js  c++  java
  • 强、双连通学习笔记

    updata:修正错误

    有向图强连通分量

    有向图强连通分量就是在一个强连通分量里面,每个点都能到达分量里面其他所有点。(很明显的是每个点只能属于一个分量)

    那么,如何求?

    Tarjan算法

    例题

    定义

    我们定义一个low数组与一个dfs数组与一个ts(时间戳,不需要过多理解,下文看了就知道功能了)

    做法

    先说DFS树,Tarjan算法实际上是利用了DFS树和时间戳进行的寻找联通分量的算法。

    一个图的DFS树是什么,对一个图进行DFS,每个点只能遍历一遍,走过的边和点构成了一棵树,例如下面这个例子。

    在这里插入图片描述

    时间戳是什么,你可以理解为访问这个点的时间,我们有个(tim)变量,每次访问一个点,就把(tim++),因此不难发现,一个点访问的早,时间戳也早。

    好,那么我们进入到DFS的世界中去:

    在这里插入图片描述

    (其中,边权为(1)的边表示不是DFS树中的边,有一条边有两个箭头的,原本是想两条有向边的,但是画图网站直接给我合并了。。。)

    这里我们对于每个点开两个变量,一个(dfn),储存时间戳,(low)刚开始储存时间戳,在中间可能更新,具体作用后面讲。

    好,在DFS中,我们总是会遇到以下两种情况:

    1. 这个点访问过了,实际上就是已经有了时间戳。
    2. 这个点没有被访问过,表现上就是这个点没有时间戳。

    第二种情况直接访问即可,因此我们要针对的是第一种情况。

    对于(4->2)这条边,表现上是(DFS)树中儿子连向父亲,我们称其为(A)类边,那么代表这个儿子和父亲实际上是一个强连通分量。

    对于(2->4)这条边,表现上是(DFS)树中父亲连向已经访问过的儿子,我们称其为(B)类边,那么这个儿子有没有可能跟这个这个父亲同一个分量呢?因为一个分量中的点能互相到达,所以默认是这个儿子来访问爸爸,而不是爸爸访问儿子,所以这个边我们先暂时不理会他。

    对于(3->5)这条边,实际表现是一个子树连向另外一个子树,我们称其为(C)类边,这种情况比较复杂,所以我们将在下文的分析后,开始这三条边的分析。

    (为了方便下文讨论,我们称第二种情况的边是(D)类边)

    对于一个联通分量,两个点必定能互相访问(但是需要注意的是,(x->y)的路径和(y->x)的路径可能有相同的边,比如:(1->2,2->3,3->4,3->1,4->2)),加入这个分量中有一个点最先被访问,我们设他是(fa)吧,好,这个时候(low)派上了用场,如果一个点(dfn==low),那么其是(fa)点,反之不是。

    先假如图中只有一个联通分量,那么有以下几个引理:

    1. 每个点都会被访问到。

      证明:如果(x)没有被访问到,假设(fa)(x)的路径是:(fa->a_1->a_2->...->a_k->x),那么(a_k)是会访问(x)的,什么?(a_k)也没有被访问,看(a_{k-1}),一直看下去,(fa)总被访问了吧。

    2. 一个分量的DFS树不一定只是一条链。

      你问我这个怎么证明?构造法啊。

      在这里插入图片描述

      看,如果先更新了(3),那么(DFS)树就变成了(1)有两个儿子(2,3)

    3. 如果(x)有一条边指向已经被访问过的点(y),我们采用(low[x]=min(low[y],low[x]))的更新方式,且如果(x)访问到未访问点(y),在(y)完成(DFS)之后,也执行上述语句,那么除了(fa)点外,每个点都不可能(low==dfn)

      证明:假如一个点,如果一个点,或者其DFS子树中有一个点可以到达(fa),那么这个点绝对不可能是(low==dfn),那么对于一个点(x),其一定存在一条到(fa)的路径,但是为什么其子树没有呢?还记得(2)中的例子吗,说明其路径中有点(y)已经被访问过了,如果假设(y)(low)不等于(y)(dfn),因为(y)(x)先访问,所以(x)也满足要求,而刚开始(DFS)时,第一个访问到已访问点的点,一定满足(low)不等于(dfn)

      但实际上,(low[x]=min(low[x],dfn[y]))也不会错(但是需要注意,(D)类边的更新方式永远都是(low[x]=min(low[y],low[x]))),因为已访问点(y)(x)先访问,所以也可以满足条件。

    但是在实际使用中,是不可能只有一个联通分量的,因此,我们可以把一个强连通分量看成一个点,那么整个图就会变成DAG图,这个方法叫做缩点。

    如这个例子,把左边变成了右边。

    在这里插入图片描述

    我们在(DFS)中,我们搜到一个分量中的一个点,上文讲了,称这个点为(fa),那么所有这个分量的点都在其子树中,还记得上面的三条边吗。

    (A)类边其实就是更新(low)的途径。

    (B)类边毫无作用,因为如果这个儿子跟我是同一个分量,设这条边为(x->z),则(z)(x)的DFS树上的一个儿子(y)的子树中,那么可以直接通过(y)完成更新,完全没有必要考虑这种情况,但是考虑了也不会错,因此在打代码的过程中可以直接考虑,减少代码量(不难发现,在(min(low[x],dfn[y]))(min(low[x],low[y]))两种更新模式都不会错)。

    (C)类边,上文说了比较复杂,如:(x->y),有可能(x,y)不是同一个分量的,那么(y)已经被先访问了,时间戳也比(x)小,那不就更新了吗?但是(y)所在的分量的(fa)节点是否已经结束(DFS)了呢,如果没有,说明(y)所在分量的(fa)(x)在DFS树中的父亲,换而言之,(x)可以到达其父亲节点,那么(x)确实不可能是(fa)节点,而且是和(y)是同一个分量的矛盾,所以其(fa)一定已经停止(DFS)了(一个分量的(fa)停止了思考,表示这个分量的点都被找过了)。因此我们可以设置(be)数组,表示这个点所属的分量的(fa)节点是否已经停止了思考,然后只用(be)(0)的点更新自身即可。

    (D)类边,只需要担心(x)在其(DFS)子树中如果有不是跟(x)同个联通块的怎么处理。

    需要证明一个东西,一个联通块的除(fa)以外的点的父亲节点都是这个联通块的点,根据引理1,每个点都会被访问,如果这个点被非联通块的点(x)访问了,那么(fa)可以到(x)(x)可以到这个点,那么这个联通块的每个点都可以通过(fa)(x)(x)可以通过这个点到每个点,那么(x)也是这个联通块的点,矛盾,证毕。

    所以如果(x)通过(D)类边访问到了(y)(y)肯定是(fa)节点,假设(y)(low==dfn),那么(x)也肯定是正确的啦,而(x)的子树中一定存在一个(fa)点,其子树中不存在(fa)点,但是这个(fa)点一定满足要求吗?但是对于这个点而言(ABC)类边都是正确的,所以这个(fa)点也是正确的,证毕。

    至于如何处理(be)吗,考虑到一个一个分量在(DFS)中的连续性,我们可以用栈储存,访问时,加入栈,当(fa)点回溯时,把(fa)点以及(fa)点以上的点全部弹出,根据数学归纳法,一定存在一个(fa),其子树的不存在(fa),这样肯定可以,而且访问(fa)之前和访问(fa)之后的栈是一样的,因此可以不断的把底层的分量删除,最终删完整个图。(其实细心的人不难发现,这不是缩点后的DAG的拓扑排序吗,这也是为什么说联通块编号是反着的拓扑序的原因)

    low,dfn的相等情况证明

    那么为什么(x)(low)不一定等于分量中(fa)(dfn)呢?

    这还不简单,构造法啊。

    在这里插入图片描述

    如果(2)先访问了(4),那么(4)(low)并不会等于(1),当然,这并不代表我们的做法是错的,只是单纯的因为(1)(4)再回到(1)必须经过(4)而已,当然点双需要注意这个东西(后文会讲)。

    扩展芝士:

    当然,现在我们尝试证明存在(fa->x->fa)的一条路径使得路径上没有一个点重复走过,是(low[x]=dfn[x])的充分不必要条件。(在(min(low[x],low[y]))的更新条件下)

    充分性:

    这样子的话,设路径为(fa->a_1->a_2->...->a_q->x->b_1->...->b_p->fa),然后(x)访问到了(b_i),发现(b_i)访问过了,就把(x)换成(b_i),最终会换成一个(b_j),使得访问(b_j)时,其子树中的一个点走过(fa)

    不必要性:

    也就是证明(low[x]=dfn[x])时,存在(fa->x->fa)的一条路径使得路径上没有一个点重复走过不成立,证明逆否命题是错的。

    不存在(fa->x->fa)的一条路径使得路径上没有一个点重复走过时,则(low[x]≠dfn[x])

    事实上:

    在这里插入图片描述

    这张图中,如果(2)优先访问了(3),什么事都没有。

    证毕

    当然,至于(min(low[x],dfn[y]))的更新方式吗。懒得想

    DFS处理问题

    需要注意的一件事情是,一次(DFS)不一定可以搞出所有的点,所以需要判断每个点有没有(DFS)过,当然,也可以设置一个超级根节点,把这个点跟所有点连起来。

    代码

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using  namespace std;
    int  belong[61000],dfn[61000],cnt,low[61000],n,m,id;
    struct  node
    {
        int  y,next;
    }a[210000];int  len,last[61000];
    void  ins(int  x,int  y)
    {
        len++;
        a[len].y=y;a[len].next=last[x];last[x]=len;
    }//边目录
    int  sta[61000],p;//栈
    bool  v[61000];//所在的分量的fa找完没?其实就是上文的be数组
    void  dfs(int  x)
    {
        dfn[x]=low[x]=++id;sta[++p]=x;
        for(int  k=last[x];k;k=a[k].next)
        {
            int  y=a[k].y;
            if(dfn[y]==0)
            {
                dfs(y);//便历
                low[x]=min(low[x],low[y]);
            }
            else  if(v[y]==false)low[x]=min(low[x],dfn[y]);//low[x]=min(low[x],low[y]);也不会错
        }
        if(low[x]==dfn[x])
        {
            int  now=0;cnt++;
            do
            {
                now=sta[p--];
                v[now]=true;//找完了
                belong[now]=cnt;//所在的分量
            }while(now!=x  &&  p>0);
        }
    }
    int  main()
    {
        scanf("%d%d",&n,&m);
        for(int  i=1;i<=m;i++)
        {
            int  x,y;scanf("%d%d",&x,&y);
            ins(x,y);
        }
        for(int  i=1;i<=n;i++)
        {
            if(dfn[i]==0)dfs(i);//遍历
        }
        printf("%d
    ",cnt);
        return  0;
    }
    

    练习

    1

    1

    这道题目难度有点大,我们做一遍Tarjan算法,然后把每个强连通分量当成一个点,计算每个点的入度与出度,我们需要知道,为什么这些点(我们已经把所有强连通分量缩点了)不在一个强连通分量里面?

    比如:
    在这里插入图片描述
    我们可以姑且的认为,一个长得像(1->2->3->4->...)的点叫伪点(非专业术语)

    而一个伪点一般有一个点入度为0,一个点出度为0,当然,即使有特殊情况使得某个为0也是没问题的,代表他和其他伪点已经有联系了。

    那么,我们只需要把一个伪点没入度的连向没出度的(当然,只有一个分量的话要特判,直接输出0),也就是max(rdcnt,cdcnt)。

    虽然很难理解,但是画以下图就知道了。

    #include<cstdio>
    #include<cstring>
    #include<cstdlib>
    using  namespace  std;
    int  flog[21000],fa[21000],biao[21000],id,n,m,cnt,t;
    struct  node
    {
        int  x,y,next;
    }a[51000];int  last[21000],len,list[21000],top;
    bool  v[21000];
    void  ins(int  x,int  y)
    {
        len++;
        a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
    }
    inline  int  mymin(int  x,int  y){return  x<y?x:y;}
    inline  int  mymax(int  x,int  y){return  x>y?x:y;}
    void  dfs(int  x)
    {
        fa[x]=biao[x]=++id;
        list[++top]=x;v[x]=true;
        for(int  k=last[x];k;k=a[k].next)
        {
            int  y=a[k].y;
            if(biao[y]==0)
            {
                dfs(y);
                fa[x]=mymin(fa[x],fa[y]);
            }
            else
            {
                if(v[y]==true)fa[x]=mymin(fa[x],fa[y]);
            }
        }
        if(biao[x]==fa[x])
        {
            int  i=0;cnt++;
            while(i!=x)
            {
                i=list[top--];
                flog[i]=cnt;
                v[i]=false;
            }
        }
    }
    int  rd[21000],cd[21000];
    int  main()
    {
        //freopen("b.in","r",stdin);
        //freopen("1.out","w",stdout);
        scanf("%d",&t);
        while(t--)
        {
            memset(fa,0,sizeof(fa));
            memset(biao,0,sizeof(biao));cnt=0;len=0;id=0;
            memset(last,0,sizeof(last));top=0;
            memset(rd,0,sizeof(rd));memset(cd,0,sizeof(cd));
            scanf("%d%d",&n,&m);
            int  ans1=0,ans2=0;
            for(int  i=1;i<=m;i++)
            {
                int  x,y;scanf("%d%d",&x,&y);
                ins(x,y);
            }
            for(int  i=1;i<=n;i++)
            {
                if(biao[i]==0)dfs(i);
            }
            if(cnt==1)
            {
                printf("0
    ");
                continue;
            }
            for(int  i=1;i<=m;i++)
            {
                int  tx=flog[a[i].x]/*缩点*/,ty=flog[a[i].y];
                if(tx!=ty)
                {
                    rd[ty]++;cd[tx]++;
                }
            }
            for(int  i=1;i<=cnt;i++)
            {
                if(rd[i]==0)ans1++;
                if(cd[i]==0)ans2++;
            }
            printf("%d
    ",mymax(ans1,ans2));
        }
        return  0;
    }
    

    2

    2

    我们先跑一遍二分匹配,然后把原本的边反向建(母牛连向公牛),并且连一条边,公牛连向他匹配的母牛,那么再跑一边强连通,我们就会发现每个分量里面都是公牛->母牛->公牛->母牛...

    也就是说每个母牛至少有两个选择,公牛也是,然后我们在找公牛能****(手动打码)的每个母牛,如果母牛跟公牛在同一分量中,那么这个母牛原本的公牛也可以在找另外一头母牛,是不是很厉害?

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<bitset>
    using  namespace  std;
    const  int  cc=4010;
    struct  node
    {
    	int  y,next;
    }a[200010];int  len,last[cc];
    struct  trlen
    {
    	int  x,y,next;
    }map[200010];int  tlen,tlast[cc];
    int  cnt,id,p;
    int  sta[cc],low[cc],dfn[cc],belong[cc];
    int  chw[cc],match[cc],n;
    bool  v[cc];
    void  ins(int  x,int  y)
    {
    	len++;
    	a[len].y=y;a[len].next=last[x];last[x]=len;
    }
    void  ins1(int  x,int  y)
    {
    	tlen++;
    	map[tlen].x=x;map[tlen].y=y;map[tlen].next=tlast[x];tlast[x]=tlen;
    }
    bool  find(int  x)
    {
    	for(int  k=tlast[x];k;k=map[k].next)
    	{
    		int  y=map[k].y;
    		if(chw[y]!=id)
    		{
    			chw[y]=id;
    			if(match[y]==0  ||  find(match[y])==true)
    			{
    				match[y]=x;
    				return  true;
    			}
    		}
    	}
    	return  false;
    }
    void  dfs(int  x)
    {
    	low[x]=dfn[x]=++id;v[x]=true;sta[++p]=x;
    	for(int  k=last[x];k;k=a[k].next)
    	{
    		int  y=a[k].y;
    		if(low[y]==0)
    		{
    			dfs(y);
    			low[x]=min(low[x],low[y]);
    		}
    		else  if(v[y]==true)low[x]=min(low[x],dfn[y]);
    	}
    	if(low[x]==dfn[x])
    	{
    		int  now=0;cnt++;
    		do
    		{
    			now=sta[p--];
    			v[now]=false;
    			belong[now]=cnt;
    		}while(now!=x);
    	}
    }
    int  main()
    {
    	scanf("%d",&n);
    	for(int  i=1;i<=n;i++)
    	{
    		int  kkk=0;scanf("%d",&kkk);
    		for(int  j=1;j<=kkk;j++)
    		{
    			int  x;scanf("%d",&x);
    			ins1(i,x+n);
    		}
    	}
    	for(int  i=1;i<=n;i++)
    	{
    		id++;
    		find(i);
    	}//二分匹配
    	for(int  i=1;i<=n;i++)ins(match[i+n],i+n);
    	for(int  i=1;i<=tlen;i++)ins(map[i].y,map[i].x);
    	for(int  i=1;i<=n;i++)
    	{
    		if(low[i]==0)dfs(i);
    	}
    	for(int  i=1;i<=n;i++)
    	{
    		int  jj=belong[i],ansl[cc];
    		ansl[0]=0;
    		for(int  j=tlast[i];j;j=map[j].next)
    		{
    			if(belong[map[j].y]==jj)ansl[++ansl[0]]=map[j].y-n;
    		}
    		sort(ansl+1,ansl+1+ansl[0]);
    		for(int  j=1;j<ansl[0];j++)printf("%d ",ansl[j]);
    		printf("%d
    ",ansl[ansl[0]]);
    	}
    	//输出
    	return  0;
    }
    

    3

    3

    这道题比较简单,如果一个强连通分量有边连向其他分量,这个分量都没用了。

    #include<cstdio>
    #include<cstring>
    using  namespace  std;
    inline  int  mymin(int  x,int  y){return  x<y?x:y;}
    int  n,m;
    int  low[21000],dfn[21000],belong[21000],cnt,out[21000],stp;
    int  sta[21000],tp=0;bool  v[21000];
    struct  node
    {
    	int  x,y,next;
    }a[21000];int  last[21000],len;
    int  ansl[21000];
    void  ins(int  x,int  y)
    {
    	len++;
    	a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
    }
    void  dfs(int  x)
    {
    	low[x]=dfn[x]=++stp;v[x]=true;sta[++tp]=x;
    	for(int  k=last[x];k;k=a[k].next)
    	{
    		int  y=a[k].y;
    		if(low[y]==0)
    		{
    			dfs(y);
    			low[x]=mymin(low[x],low[y]);
    		}
    		else  if(v[y]==true)low[x]=mymin(low[x],dfn[y]);
    	}
    	if(low[x]==dfn[x])
    	{
    		int  now=0;cnt++;
    		while(now!=x)
    		{
    			now=sta[tp--];
    			belong[now]=cnt;
    			v[now]=false;
    		}
    	}
    }
    int  main()
    {
    	memset(v,true,sizeof(v));
    	while(scanf("%d",&n)!=EOF)
    	{
    		if(n==0)break;
    		scanf("%d",&m);
    		memset(low,0,sizeof(low));stp=0;
    		memset(last,0,sizeof(last));len=0;
    		memset(out,0,sizeof(out));
    		for(int  i=1;i<=m;i++)
    		{
    			int  x,y;scanf("%d%d",&x,&y);
    			ins(x,y);
    		}
    		for(int  i=1;i<=n;i++)
    		{
    			if(low[i]==0)dfs(i);
    		}
    		for(int  i=1;i<=m;i++)
    		{
    			if(belong[a[i].x]!=belong[a[i].y])out[belong[a[i].x]]++;
    		}
    		for(int  i=1;i<=n;i++)
    		{
    			if(out[belong[i]]==0)ansl[++ansl[0]]=i;
    		}
    		for(int  i=1;i<ansl[0];i++)printf("%d ",ansl[i]);
    		printf("%d
    ",ansl[ansl[0]]);ansl[0]=0;
    	}
    	return  0;
    }
    

    双连通分量

    双联通分量就是无向图的强连通分量

    UPDATA:由于后面又更新了博客,所以有些内容看起来比较神奇,可能就是前一次的内容和后一次的更新内容卡一块了。

    边-双连通分量

    如果原本两个点是连通的,截断一条边就使得两个点不联通了,这条边叫桥。

    在这里插入图片描述

    边双

    定义

    边双连通分量就是分量中不含桥的联通块。

    一个点只会属于一个边双,如果属于两个边双,可以把这两个边双合并(因为如果这两个边双间有桥,删掉后,有(x),不会不连通,所以矛盾,不存在桥)。

    做法

    没错,Tarjan永远的神!!!!

    跟强连通一样的更新套路,但是需要注意的一件事情是,不可能存在(C)类边(比较重要的性质),因为如果(x->y),那么理论上来讲,因为这是双向边,所以(y)(DFS)的时候就可以访问(x)。而且需要注意。需要保存父亲边(需要注意的是,这里保存的是父亲边,因为可能存在重边),不能走过父亲边(但是如果题目中要求重边视为一条,则保存父亲),所以不需要保存(be)数组。

    至于判断条件,有两种方法。

    1. 对于(low[x]==dfn[x])时,其到父亲的边是桥(但是如果(x)(DFS)树的根则没有父亲),且(x)(fa)点。

      这种跟强连通是比较像的,仔细想想可以发现,栈的证明是一样的。应该

    2. 对于(x)(DFS)树中的一个儿子(y),若(low[y]>dfn[x]),则(x-y)的边是桥,(y)(fa)节点,需要注意的是,这种写法,栈的弹出设定不应该是:(sta[top]!=x),而应该是(now!=y),因为(y,x)在栈中不一定连续,可能存在(x)的另外一个儿子和(x)是一个边双的。

    缩点

    缩点都是差不多的,但是无向图的缩点最后会变成树,而不是DAG,后面的点双也是。

    dfn,low相等情况证明

    需要注意的是,由于边双中,(fa->x->fa)的路径中存在点重复无所谓,所以(min)中可以(dfn,low)都可以,而其(low[x])(dfn[fa])的等于情况和强连通基本相同(好像就是相同的),所以不做过多分析。(反正不存在(C)类边,随便分析)

    DFS处理

    由于是无向图,如果图联通,只需要跑一次(DFS),点双边双都一样,不需要像强连通一样用循环判断每个点时候(DFS)了。当然如果图不连通还是要的

    代码

    代码:

    void  dfs(int  x,int  fa)
    {
    	low[x]=dfn[x]=++stp;
    	sta[++tp]=x;
    	for(int  k=last[x];k;k=a[k].next)
    	{
    		int  y=a[k].y;
    		if(y!=fa)
    		{
    			if(low[y]==0)
    			{
    				dfs(y,x);
    				low[x]=min(low[x],low[y]);
    			}
    			else  low[x]=min(low[x],dfn[y]);
    		}
    	}
    	if(low[x]==dfn[x])
    	{
    		int  now=0;cnt++;
    		while(now!=x)
    		{
    			now=sta[tp--];
    			belong[now]=cnt;
    		}
    	}
    }
    

    练习

    这次没例题,直接放练习

    练习

    像上次那样,我们记录每个分量的度(无向边),为0,ans+=2,为1,ans++

    然后答案为ans/2+ans%2

    #include<cstdio>
    #include<cstring>
    using  namespace  std;
    inline  int  mymin(int  x,int  y){return  x<y?x:y;}
    int  low[6000],dfn[6000],belong[6000],cnt,stp;
    int  sta[6000],tp;
    struct  node
    {
    	int  x,y,next;
    }a[21000];int  last[6000],len;
    int  ax[11000],ay[11000],n,m,io[11000],ans;
    void  ins(int  x,int  y)
    {
    	len++;
    	a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
    }
    void  dfs(int  x,int  fa)
    {
    	low[x]=dfn[x]=++stp;
    	sta[++tp]=x;
    	for(int  k=last[x];k;k=a[k].next)
    	{
    		int  y=a[k].y;
    		if(y!=fa)
    		{
    			if(low[y]==0)
    			{
    				dfs(y,x);
    				low[x]=mymin(low[x],low[y]);
    			}
    			else  low[x]=mymin(low[x],dfn[y]);
    		}
    	}
    	if(low[x]==dfn[x])
    	{
    		int  now=0;cnt++;
    		while(now!=x)
    		{
    			now=sta[tp--];
    			belong[now]=cnt;
    		}
    	}
    }
    int  main()
    {
    	scanf("%d%d",&n,&m);
    	for(int  i=1;i<=m;i++)
    	{
    		scanf("%d%d",&ax[i],&ay[i]);
    		ins(ax[i],ay[i]);ins(ay[i],ax[i]);
    	}
    	for(int  i=1;i<=n;i++)
    	{
    		if(low[i]==0)dfs(i,0);
    	}
    	if(cnt==1){printf("0
    ");return  0;}//特判
    	for(int  i=1;i<=m;i++)
    	{
    		if(belong[ax[i]]!=belong[ay[i]])io[belong[ax[i]]]++,io[belong[ay[i]]]++;
    	}
    	for(int  i=1;i<=cnt;i++)
    	{
    		if(io[i]==0)ans+=2;
    		else  if(io[i]==1)ans++;
    	}
    	printf("%d
    ",ans/2+ans%2);
    	return  0;
    }
    

    点双连通分量

    割点

    就是把点割掉后,会导致原本联通块变成多个联通块,那么这个点就是割点。

    点双连通分量

    点双就是不含割点的连通分量(特别的,两个点一条边也是一个点双)。

    点双最麻烦的就是一个点可能属于多个分量,如果一个点属于多个连通分量,此时这个点一定是割点,而割点也必然属于多个联通块,如下图。

    在这里插入图片描述

    我们发现,中间有一个点属于两个连通分量。

    性质:

    1. 点双每两个点存在一个简单环,每三个点(x,y,z),一定存在(x->y->z)的一条简单路径。(应该吧,这个在下文小结给出的扩展资料有证明)
    2. 我们探究一下边双和点双的关系,不难一个桥左右两个点一定是割点,而一条边左右两个点是割点,但这条边不一定是桥,好,这样的话,一个边双中可能含有很多个割点,只要不含桥即可,因此一个边双可以被拆分成多个点双,而且,点双中一定不存在桥。
    3. (x)属于点双(A),属于边双(B),那么(A∩B=B),证明:如果存在(y)不属于(B)而属于(A),那么(x,y)之间存在简单环(点双的),环上不存在割边,那么(y,x)应该属于同一个边双,因此矛盾,证毕。

    点双其实可以看作边的联通关系,因为每条边最多属于一个分量(这个证法就是删去证联通)。

    点双依旧不存在(C)类边,且点的在(DFS)树中的连续性也是可以证明的,证法依旧类似。

    但是突然发现一个十分重要的事情:如果不存在一条(fa->x->fa)的路径使得路径上点不重复(不包括起终点)的话,那么(x,fa)不可能是一个分量中的(实际上说明这个(fa)不是(x)所在连通分量的(fa))。我们当时证明了,如果我们采用了(min(low[x],low[y]))的更新方式,那么(low[x]=dfn[fa]),这个是很明显的错误,因此我们只能

    那么就只能采用(min(low[x],dfn[y]))的更新方法了,但是这个为什么对呢?扩展资料中证明了,两个点之间不存在简单环,则一定存在割点,那么这种方法更新的话,(x)最多就等于(x,fa)中间那个割点的(dfn),因此不用担心此情况,好,那么存在简单环就一定对吗?

    好,就证明一个引理:

    (x-a_1-a_2-a_3.-..-a_q-y),其中(x)准备(DFS)(x)在DFS树中在(y)的子树中,(a_i(1≤i≤q))没有被访问过,那么在(x)(DFS)完之后,(low[x]≤dfn[y])。假设(a_i)是下一个(DFS)的对象,那么只要(a_i-...-y)成立即可,运用数学归纳法,发现两个点一定成立,则数学归纳法生效。

    好,现在证明点双中的除(fa)外每个点的(low)都小于(dfn[x])(需要注意的是,本文的证明的(fa)都不是指父亲节点,而是指联通块(fa)节点,父亲节点我会特地的打中文)。

    假设存在简单环:(fa-a_1-a_2-...-a_q-fa),第一个被访问的点是(a_i),那么(a_i)一定等于的(low)一定等于(dfn[fa]),好,(a_i)到达(a_j),那么,(a_j)(low)也等于(dfn[fa]),但是环上(a_i-a_j)中间的点呢?他们的(low)说不定不等于(dfn[fa]),于是我们继续利用数学归纳法,已知(a_i-a_j)中最先被访问的点是(a_k),那么(a_k)(low)小于等于(dfn[a_i]),小于(dfn[a_k]),然后把区间切成了:(a_i-a_k,a_k-a_i),最终一定会被切成两个点,中间没有任何点,证毕。

    好,然后直接用(min(low[x],dfn[y]))更新方法即可,当然,(D)类边一直不变都是用(low[y])更新的。

    但需要注意的是,点双不需要保留父亲边,允许走父亲,反正每个点肯定和其父亲是同一个点双(因为两个点也是点双,至少有个保底)。

    什么,判定一个点是不是(fa)节点?如果(x)通过(D)类边到(y),然后(low[y]==dfn[x])那么(x)(fa)节点,和(y)一个点双。

    点双缩点

    由于每个割点,属于多个联通块,不能直接缩,因此,我们选择保留割点,删去所有的非割点,如果割点(x)属于联通块(y),那么(x)(y+n)连一条边即可。

    点双的点

    也许就有人要问了,MAD,一个点可以属于多个点双,还要栈干什么,反正每个点的(belong)可以有多个?但是在点双缩点的恰恰就需要找出一个点双中的点(但是不用保存到后面继续使用,而是直接用链式前向星缩点建树),这个时候了,我们发现一个点在结束完(DFS)后,就不会作为(fa)节点了,所以需要特殊考虑的就是当前(DFS)的节点(x)

    难道是如果(x)(fa)节点的话,且和(y)一个分量,就把栈中(x)(x)以上的全部弹出,然后再把(x)加回队列中?

    不,类似边双的问题(x,y)不一定连续,所以应该把(y)(y)以上的点全部弹出,并算上(x)即可。

    点双的边

    当然,有的时候需要搞出一个点双中所有的边,但是边只属于一个点双,就比较方便,直接在(DFS)中往栈塞(D)类边,但是(A、B)类边呢?因为双向边,所以(A=B),所以只需要保存比较容易处理的(A)类边即可,然后在弹出的时候判断栈顶是不是(x->)y的(D)类边即可。

    不对!!!!因为允许到父亲,所以实际上(D⊂A),所以,几种处理方法:

    1. (v)表示这个边是否用过。
    2. 不保存(D)类边,但是这样栈弹出的否决条件就比较难处理,因此改进的方法可以是优先到父亲,或者说是到父亲的边不入栈。

    当然还有其他方法,不再赘述。

    代码

    int  dfn[61000],cnt,low[61000],id;
    int  sta[61000],p;
    void  dfs(int  x)
    {
        dfn[x]=low[x]=++id;sta[++p]=x;
        for(int  k=last[x];k;k=a[k].next)
        {
            int  y=a[k].y;
            if(!dfn[y])
            {
                dfs(y);
                low[x]=min(low[x],low[y]);
                if(low[y]==dfn[x])//我和你构成一个点双
                {
                    int  now=0;cnt++;
                    while(now!=y  &&  p>0)now=sta[p--];
                    //可以在最后处理一下x
                }
            }
            else  low[x]=min(low[x],dfn[y]);
        }
    }
    

    初始化问题

    其他自己想,三个分量都差不多,这里只说栈。

    其他两个分量一个点只属于一个分量,所以最后栈中一个点不剩,

    但是边双最后会剩一个点,(DFS)树的根,所以在初始化的时候别忘了(top=0;)

    求割点与桥

    割点

    例题

    我们研究DFS序就会发现,只要一个不是根结点的其中一个儿子的low小于等于(如果可以走父亲,可以直接换成等于)他的dfn,那么这个点就是割点,根节点就是他所在的点双个数大于等于(2),当然具体表现就是(D)类边的个数,因为根节点的(dfn)最小,不可能小于,只能等于。

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using  namespace std;
    int  dfn[61000],low[61000],n,m,id;
    struct  node
    {
        int  y,next;
    }a[210000];int  len,last[61000];
    void  ins(int  x,int  y)
    {
        len++;
        a[len].y=y;a[len].next=last[x];last[x]=len;
    }
    
    int  ans[130000];
    void  dfs(int  x,bool  type)//判断是不是根节点而已
    {
    	bool  bk=false;int  cnt=0;
        dfn[x]=low[x]=++id;
        for(int  k=last[x];k;k=a[k].next)
        {
            int  y=a[k].y;
            if(!dfn[y])
            {
                cnt++;
                dfs(y,0);
                low[x]=min(low[x],low[y]);
                if(low[y]==dfn[x]/*桥就是>*/)bk=true;
            }
            else  low[x]=min(low[x],dfn[y]);
        }
    	if(type)//根节点特判 
    	{
    		if(cnt>=2)ans[++ans[0]]=x;
    	}
    	else  if(bk==true)ans[++ans[0]]=x;
    }
    int  main()
    {
        scanf("%d%d",&n,&m);
        for(int  i=1;i<=m;i++)
        {
            int  x,y;scanf("%d%d",&x,&y);
            ins(x,y);ins(y,x);
        }
        for(int  i=1;i<=n;i++)
        {
            if(dfn[i]==0)dfs(i,1);
        }
        sort(ans+1,ans+ans[0]+1);
        printf("%d
    ",ans[0]);
        for(int  i=1;i<ans[0];i++)printf("%d ",ans[i]);
        if(ans[0])printf("%d
    ",ans[ans[0]]);
        return  0;
    }
    

    桥就不打了吧,直接在边双的时候随便乱搞都可以啦。

    看个人喜好吧。

    小结

    又水了一篇博客

    补充资料:我博客中的另外一篇博客关于“双联通分量的存在条件的证明(对于算法进阶的补充)”,应该可以加深你对点双的理解。

    对于练习中有些题目的证明,强烈建议去网上搜证明,我的这个太不严谨了。(其实就是懒得更新练习了)

  • 相关阅读:
    python基础
    python基础
    python基础
    在hive下使用dual伪表
    mariadb 压缩包gz安装方式
    linux下 mysql5.7.20安装(精华)
    在开启kerberos 后,hbase存在数据命名空间的问题(解决方案)
    LINUX下解决TIME_WAIT等网络问题
    常用Oracle进程资源查询语句(运维必看)
    linux 下oracle 11g静默安装(完整版)
  • 原文地址:https://www.cnblogs.com/zhangjianjunab/p/10701255.html
Copyright © 2011-2022 走看看