连通性问题

强连通分量

对于一张有向图,对于图中任意两个节点\(x,y\)\(x\)能到\(y\)\(y\)也能到\(x\),则称其为强连通图。有向图的极大联通子图被称为强连通分量,简记为SCC(Strongly Connected Component)。

有时候,我们需要将一张有向图分成几个强连通分量,这时候可以基于Tarjian设计一个算法。

Tarjian求强连通分量

按照Tarjian的惯例,我们需要对整个图进行一次dfs,将图的搜索树求出,设\(dfn_x\)\(x\)节点最先遍历到的时间戳。

对于一个节点\(x\)来说,如果有一条边连接了它与它搜索树中的祖先\(fa_x\)(不仅是父亲),说明这个点可以与树上\(fa_x\)\(x\)的路径组成环,而显然地一个环就是一个强连通子图,有助于我们寻找强连通分量。同理,如果有一点\(z\),它非\(x\)的祖先,但是它存在路径可以到达\(fa_x\),那连接\(x\)\(z\)的边也很重要。

Tarjian维护了一个栈,栈中保存了\(x\)节点的祖先\(fa_x\),与已经访问过,并且存在一条路径到达\(x\)祖先的点。

接下来为了寻找到这些边,我们需要引入追溯值\(low_x\)。追溯值定义为满足以下条件的节点的最小时间戳:
1.该点在栈中。
2.存在一条从以\(x\)为跟的子树内出发指向\(x\)的有向边。

\(low_x\)的计算过程如下:
1.当节点\(x\)第一次访问时,将\(x\)入栈,并初始化\(low_x=dfn_x\).
2.遍历从\(x\)出发的每条边\((x,y)\):
(1).如果\(y\)未被访问,则\(y\)\(x\)的子树内节点,它的追溯值可以更新\(low_x\),所以访问\(y\),回溯后令\(low_x=min(low_x,low_y)\)
(2).若\(y\)被访问并且在栈内,则说明\(x\)可以到达这个节点后在回到自己,根据定义可以使\(low_x=min(low_x,dfn_y)\)

下面我们来看一看如何分割强连通分量:
判定法则很简单:\(low_x=dfn_x\),则此时栈中从\(x\)到栈顶的所有节点构成一个强连通分量。

如果\(low_x=dfn_x\),那么说明\(x\)的子树内节点不能到达比\(x\)更早的节点再回到\(x\)(指时间戳更小),所以能形成的最大的强连通分量最大只包含了\(x\)

以上便是\(Tarjian\)求强连通分量的主要流程。

实现

我们可以设\(ins_x=1\)表示\(x\)此刻是否在栈中,只在遍历边\((x,y)\)\(y\)被访问过且\(ins_y=1\)时才将\(low_x\)更新为\(min(low_x,dfn_y)\),代表\(x\)可以去到\(y\)在回来。

接下来是tarjian代码:

int dfn[maxn],low[maxn],num;
int sta[maxn],top;
int ins[maxn],cou[maxn],cnt
void tarjian(int now) {
	dfn[now]=low[now]=++num;
	sta[++top]=now; ins[now]=1;
	for(int i=head[now];i;i=nex[i]) {
		int st=to[i];
		if(!dfn[st]) {
			tarjian(st);
			low[now]=min(low[now],low[st]);
		}
		else if(ins[st]) low[now]=min(low[now],dfn[st]);
	}
	if(dfn[now]==low[now]) {
		int x; cnt++;
		do {
			x=sta[top--];
			ins[x]=0; 
			cou[x]=cnt;
		} while(x!=now); 
	}
}

割边&边双连通分量

Tarjian求割边:

对于图中的一条边,如果将该边删去后图的连通性遭到破坏,则称改变为该图的割边。

我们现在来阐述一下这种情况下的追溯值\(low_x\)定义中需要满足的条件:
1.节点在搜索树中以\(x\)为根的子树内。
2.通过一条不在搜索树上的边,可以到达以\(x\)为根的子树内的节点。

搜索过程中\(low_x\)的计算过程如下:
对于一条边\((x,y)\)
1.若\(x\)为搜索树上\(y\)的父节点,则\(low_x=min(low_x,low_y)\)
2.若无向边不是搜索树上的边,则\(low_x=min(low_x,dfn_y)\)>

接下来是割边的判定法则:对于搜索树上的一个节点\(x\)的子节点\(y\),如果满足\(dfn_x<low_y\),则可以说边\((x,y)\)为一条割边。

根据定义,如果\(dfn_x<low_y\),则说明从\(y\)的子树内出发,不经过边\((x,y)\)无法到达点\(x\),所以如果把边\((x,y)\)删除,图将被分成两个部分,满足了割边的定义。反之则\((x,y)\)显然不为割边。

求割边实现:

注意,我们在一个点\(x\)时自然会有一条来边,此边就为\(x\)点不能讨论的搜索树上的边,我们可以递归时带上一条来边,判断反边即可。

如何判断一条边是另一条边的反边?我们可以在使用邻接表存图时将边的编号从2开始,将一条边与其反边先后存储,我们就会发现此时一条边的编号异或上1就为它的反边编号。比如2边异或上1后为3,3异或上1后为2。

下面是求割边的Tarjian代码实现:

int dfn[maxn],low[maxn],cnt;
bool bj[maxn];//是否为桥
void tarjian(int now,int in) {
	dfn[now]=low[now]=++cnt;
	for(int i=head[now];i;i=nex[i]) {
		int st=to[i];
		if(!dfn[st]) {
			tarjian(st,i);
			low[now]=min(low[now],low[st]);
			if(low[st]>dfn[now]) bj[i]=true,bj[i^1]=true;
		}
		else if(i!=(in^1)) low[now]=min(low[now],dfn[st]);
	}
}

求边双连通分量:

对于一张图,如果它不存在割边,我们就可以说它为一个边双连通图,边双连通分量就为一张图内的一个极大边双连通子图。

我们可以发现,根据定义,边双连通分量不包含任何割边,那么显然我们可以先将割边删去,整张图会被分割为几个子图,每个子图其实就是一个边双连通分量。

下面我们使用\(c_x\)表示\(x\)点属于第几个边双。

int c[maxn],num;
void dfs(int now,int fa) {
	c[now]=num;
	for(int i=head[now];i;i=nex[i]) {
		int st=to[i];
		if(c[st] || bj[i]) continue;
		dfs(st,now);
	}
}

割点

Tarjian求割点:

如果图中一个点,将其与与其相连的所有边删去后,整张图的连通性遭到破坏,则称这个点为割点。

追溯值计算方式类似割边的Tarjian,不同的是判定法则:\(x\)为割点当且仅当存在搜索树上一个\(x\)的子节点\(y\),使得\(dfn_x<=low_y\),道理也是类似的。

不过要注意如果\(x\)为搜索树的根节点,则\(x\)为割点当且仅当至少存在两个节点满足上述条件。

同时因为要求小于等于,所以不用讨论父节点之类的问题,直接上就是了。

求割点实现:

int dfn[maxn],low[maxn],cnt,num;
bool cut[maxn];
int root;
void tarjian(int now) {
	dfn[now]=low[now]=++cnt;
	int flag=0;
	for(int i=head[now];i;i=nex[i]) {
		int st=to[i];
		if(!dfn[st]) {	
			tarjian(st);
			low[now]=min(low[now],low[st]);
			if(low[st]>=dfn[now]) {
				flag++;
				if(now!=root || flag>1) cut[now]=true;
			}
		}
		else low[now]=min(low[now],dfn[st]);
	}
}

求点双连通分量:

一个节点只会在一个边双之中出现,但不同地,一个割点不只会在一个点双中出现。

为了求出点双,我们需要在求割点过程中维护一个栈。当一个节点被第一次访问时,将该点假如栈;当割点判定法则成立时,无论该点\(x\)是否为根节点,都需要从栈顶不断弹出节点,直至子节点\(y\)被弹出,然后所有被弹出的节点与\(x\)一起构成一个点双。

下面是加入了求点双的tarjian代码:

void tarjian(int now) {
	dfn[now]=low[now]=++num;
	stack[++top]=now;
	for(int i=head[now];i;i=nex[i]) {
		int st=to[i];
		if(!dfn[st]) {
			tarjian(st);
			low[now]=min(low[now],low[st]);
			if(low[st]>=dfn[now]) {
				cnt++;int x;
				do {
					x=stack[top--];
					add2(x,cnt),add2(cnt,x);
				} while(x!=st);
				add2(now,cnt),add2(cnt,now);
			}
		}
		else low[now]=min(low[now],dfn[st]);
	}
}

THE END

posted @ 2023-08-14 20:59  Ian8877  阅读(19)  评论(0编辑  收藏  举报