点双连通分量

前言

由于点双和边双都是无向图里面的东西,所以下面的讲解都以图是无向图作为前提。

概念

割点: 对于一个连通图中的点 x x x,假如删去这个点以及与所有 x x x 相连的边之后图不连通,那么称 x x x 为该图的割点

点双联通的: 对于一个无向图,假如仅仅对于该图而言其中不包含割点,那么称这个图是点双连通的。(和网上的不一样?别急,往下看)

点双连通分量: 对于一个无向图中的极大点双连通的子图,我们称这个子图为点双连通分量

为了方便,下面简称点双连通分量为点双。

性质

1、 除了一种比较特殊的点双,其他的点双都满足:任意两点间都存在至少两条点不重复路径。

比较特殊的点双:
在这里插入图片描述
网上大部分以该性质作为点双的定义,然后说这种图虽然不满足定义,但是是一个特殊的点双。而上面那个更严谨的定义则可以将这种情况包含在内,更加完备。

2、 图中任意一个割点都在至少两个点双中。

因为删去割点后图会不连通,所以割点至少连接着图的两部分,而由于点双中不能有割点,所以这两部分肯定不在同一个点双中,所以割点至少存在于两个点双中。

再次提醒,点双联通的的定义是 仅仅对于该图而言 其中不包含割点,那么称这个图是点双连通的,也就是说,可以包含原图中的割点。

3、 任意一个不是割点的点都只存在于一个点双中。

这个很显然,不多解释。

找割点

d f n [ x ] dfn[x] dfn[x] 表示遍历到 x x x 之前遍历过多少个点, l o w [ x ] low[x] low[x] 表示从 x x x 出发在不经过父子边的情况下能到达的最早的祖先的 d f n dfn dfn 值。

回看割点的定义:把这个点删除后图会不连通,那么肯定存在一些点,在不经过该割点的情况下无法到达割点的祖先,用这个性质来找就好了。

代码:

int dfn[maxn],low[maxn],id;
bool cut[maxn];
void dfs1(int x,int from)//from表示x与父亲之间的边的编号
{
	dfn[x]=low[x]=++id; int son=0;
	for(int i=first[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==(from^1))continue;//不能通过反向边走回去
		if(!dfn[y])
		{
			dfs1(y,x); son++;
			if(low[y]<low[x])low[x]=low[y];//更新low,假如y能走到更小的,那么x也能走到
			if(low[y]>=dfn[x])cut[x]=true;
			//假如儿子在不经过父子边的情况下不能到达x的祖先,那么x就是个点
		}
		else if(dfn[y]<low[x])low[x]=dfn[y];//假如走到了一个dfn更小的,那么更新low
	}
	if(fa==-1&&son==1)cut[x]=false;//特判出发点在一个环中的情况
}
void tarjan()
{
	for(int i=1;i<=n;i++)//如果图不连通,那么每一个连通块都要跑一遍
	if(dfn[i]==0)dfs1(i,-1);
}

再说说那个特判,是用来处理这种情况的:
在这里插入图片描述
假如从红色节点出发,那么最后红色节点会被判定成割点,因为没有儿子能走到它的祖先,但是他压根就没有祖先啊,所以这时候,我们判一下,假如没有祖先,那么不能算割点,于是有了

fa==-1

这个判定条件。

但是

son==1

这个条件用来干啥?

我们注意到,出发点也有是割点的情况,比如:
在这里插入图片描述
如果从红色节点出发,而此时红色节点又刚好是割点,那怎么办呢?

这时候就要用到

son==1

这个限制了,因为如果起点是割点,那么 s o n son son 肯定大于 1 1 1

发现只有遍历到一个 d f n [ y ] = 0 dfn[y]=0 dfn[y]=0 y y y 时, s o n son son 才会 + 1 +1 +1,所以 s o n son son 的实际意义就是该点所连接的点双数量。

而根据上面的性质,假如 s o n = = 1 son==1 son==1,那么这个点肯定不是割点,否则一定是割点,所以在 f a = − 1 fa=-1 fa=1 的同时我们还需要进行 s o n = 1 son=1 son=1 这样的判断。

那可能有人会问了,为什么其他的割点不用 s o n = 1 son=1 son=1 这样的方法来判呢?仔细想想,其他的点和出发点的区别就是:他们都有父亲。而遍历的时候是不能往回走的,也就是说,我们不知道父亲的状态,所以不能用 s o n son son 来判。

找点双

这个时候就要请出伟大的 Tarjan 了!

我们依然考虑使用上面的 d f n dfn dfn l o w low low 来求,我们将深搜时遇到的所有加入到栈里面,当找到一个割点的时候,就将这个割点往下走到的所有边弹出,而这些边所连接的点就是一个点双了。

代码:

int dfn[maxn],low[maxn],id,t;
edge zhan[(maxn*maxn)<<1];//存边的栈
int belong[maxn],cnt;//belong记录每个点属于哪一个点双,cnt记录点双个数
bool cut[maxn];
set<int> s[maxn];//记录每个点双包含哪些点,如果题目不需要也可以不求
void dfs(int x,int from)
{
	dfn[x]=low[x]=++id; int son=0;
	for(int i=first[x];i;i=e[i].next)
	{
		if(i==(from^1))continue;
		int y=e[i].y;
		if(!dfn[y])
		{
			zhan[++t]=e[i]; dfs(y,i); son++;//先压栈再遍历
			if(low[y]<low[x])low[x]=low[y];
			if(low[y]>=dfn[x])//发现x是割点
			{
				cnt++; edge xx; cut[x]=true;
				do{
					xx=zhan[t--];//弹出
					belong[xx.x]=belong[xx.y]=cnt;//标记
					s[cnt].insert(xx.x);s[cnt].insert(xx.y);//记录
				}while(xx.x!=x||xx.y!=y);//假如已经弹到 x到y 这条边了,就停下来
			}
		}
		else if(dfn[y]<low[x])low[x]=dfn[y];
	}
	if(from==-1&&son==1)cut[x]=false;
}

附赠题表

Knights of the Round Table   题解
[HNOI2012]矿场搭建   题解
[ZJOI2004]嗅探器   题解


边双连通分量

概念

割边: 假如删去这条边后图不连通,那么称这条边为割边。

边双联通的: 对于一个图,假如仅仅对于该图而言其中没有割边,那么我们称这个图是边双联通的

性质

1、 割边不属于任意边双,而其它非割边的边都属于且仅属于一个边双。

2、 对于一个边双中的任意两个点,它们之间都有至少两条边不重复的路径。

找割边

和找割点类似,直接上代码:

int dfn[maxn],low[maxn],id;
bool cut[maxn];
void dfs(int x,int from)
{
    dfn[x]=low[x]=++id;
    for(int i=first[x];i;i=e[i].next)
    {
        if(i==(from^1))continue;
        int y=e[i].y;
        if(!dfn[y])
        {
            dfs(y,i);
            if(low[y]<low[x])low[x]=low[y];
            if(low[y]>dfn[x])cut[i]=cut[i^1]=true;
            //注意这里是大于而不是大于等于,原因想想就明白了
        }
        else if(dfn[y]<low[x])low[x]=dfn[y];
    }
}

找边双

做法1

在请出Tarjan之前,我们先介绍另外一种做法:第一次 d f s dfs dfs 找出割边,然后第二次 d f s dfs dfs 在不经过割边的情况下遍历所有点,每一次遍历经过的一个子图就是一个边双。

做法2

用类似找点双的做法,但是栈里面压点,不压边。

代码:

int dfn[maxn],low[maxn],belong[maxn],id=0,cnt=0;
int zhan[maxn],t=0;
void dfs(int x,int from)
{
	dfn[x]=low[x]=++id;zhan[++t]=x;
	for(int i=first[x];i;i=e[i].next)
	{
		if(i==(from^1))continue;
		int y=e[i].y;
		if(!dfn[y])
		{
			dfs(y,i);
			if(low[y]<low[x])low[x]=low[y];
		}
		else if(dfn[y]<low[x])low[x]=dfn[y];
	}
	if(dfn[x]==low[x])
	{
		cnt++; int xx;
		do{
			xx=zhan[t--];
			belong[xx]=cnt;
		}while(xx!=x);
	}
}

题表

Caocao’s Bridges   题解
[CERC2015]Juice Junctions   题解
[USACO06JAN]冗余路径Redundant Paths   题解

一点个人感想

对于什么时候用点双,什么时候用边双这个问题,个人感觉是,点双的性质其实比边双要好,因为边双里面允许有割点的存在,边双只强调点集内所有点能相互到达,但点双就有一些更好的性质,比如像吃掉一个点之后剩下的点之间还能相互到达。

当然这些都是乱吹的,建议不看跳过即可。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐