强、双连通学习笔记

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\n",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\n");
            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\n",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\n",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\n",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\n");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\n",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\n",ans[0]);
    for(int  i=1;i<ans[0];i++)printf("%d ",ans[i]);
    if(ans[0])printf("%d\n",ans[ans[0]]);
    return  0;
}

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

看个人喜好吧。

小结

又水了一篇博客

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

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

posted @ 2019-04-13 15:13  敌敌畏58  阅读(183)  评论(0编辑  收藏  举报