无向图连通性相关算法及概念

written on 2022-08-03

之前第一次讲的时候没有掌握得很好,这里再写一篇博客总结一下。

(注:以下均为通俗不专业定义,具体定义可以百度百科)


Part 1

先来讨论一下无向图的割点

割点:在一张无向图中,若对于点 \(x\),删去 \(x\) 以及与 \(x\) 相连的所有边后,原图分裂成两个或两个以上子图,那么称点 \(x\) 为图的一个割点。

桥:又称割边。在一张无向图中,若对于边 \(e\),删去 \(e\) 后,原图分裂成两个或两个以上的子图,那么称边 \(e\) 为图的一条割边(或桥)。

伟大的计算机科学学者 Tarjan 提出了割边判定法则与割点判定法则。 下面来介绍一下相关算法。

  1. 搜索树

对于一个无向连通图,从其中任选一个点作为根节点出发对整张图进行深度优先遍历,每个点只访问一次,此时所有发生递归的边构成了一棵树,那么称这棵树为原无向连通图的搜索树。对于一般的无向图,它将会构成一片搜索森林。

很明显搜索树不止一种形态。代码实现可以用 \(dfn\) 代表时间戳,对于未访问的节点,进行递归。

  1. 追溯值(\(low\) 数组)

在图的连通性若干问题中,这是区别与图论其他分支的一个最特殊的地方。

追溯值 \(low_x\) 定义为以下节点 \(dfn\) 的最小值:

  1. \(x\) 的子树中的节点。

  2. 通过恰好一条非树边能够到达 \(x\) 的子树中的节点。

代码实现中 \(low_x\) 的计算方法:

void tarjan(int x)
{
	dfn[x]=low[x]=++tot;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else low[x]=min(low[x],dfn[y]);
	}
}
  1. 割边判定法则/割点判定法则

无向边 \((x,y)\) 是桥,当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足 \(low_y\gt dfn_x\)

\(x\) 不是搜索树的根节点,则 \(x\) 是割点当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足 \(low_y\geq dfn_x\);反之若 \(x\) 是搜索树的根节点,则 \(x\) 是割点当且仅当搜索树上存在至少两个节点 \(y1,y2\) 满足上述条件。

完全是可以感性理解的。另外,桥一定是树边

  1. 判定割边时需要注意的成对变换问题

求割边时,需要判断 \(low_y\gt dfn_x\),此时 \(low_{son_x}\) 不应从 \(dfn_x\) 更新而来,所以此时用成对变换的技巧,即将加边时的 \(tot\)\(1\) 开始,对于一条双向边的加边,构成成对变换。

void tarjan(int x,int in_edge)
{
	...
	for(...)
	{
		if(!dfn[y]) ...
		else if((i^1)!=in_edge) low[x]=min(low[x],dfn[y]);
	}
}

由于割点的判定是取等的,所以不用成对变换也没关系。

  1. 边双/点双连通分量

先给出定义:

边双连通分量:对于一个无向图,若其去掉任意一条边都不会改变此图的连通性,即不存在桥,则称为边双连通图。一个无向图中的每一个极大边双连通子图称作此无向图的边双连通分量。

点双连通分量:若一个无向图中的去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称作点双连通图。一个无向图中的每一个极大点双连通子图称作此无向图的点双连通分量。

(节选自百度百科,这个定义还是比较好的。)

三点注意:

  • 连接某两个边双连通分量的边即是桥。

  • 一个割点可能存在于多个点双连通分量。

  • 一个点双连通分量内可能存在任意多个割点(也可能没有)。

然后介绍一下二者的缩点:

对于边双的缩点,很显然可以直接找出所有的桥,然后用 \(dfs\) 找连通块染色即可。


void dfs(int x)
{
	dcc[x]=dc;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(dcc[y]||bri[i]) continue;
		dfs(y);
	}
}
int main()
{
	...
	for(int i=1;i<=n;i++) if(!dcc[i]) ++dc,dfs(i);
}

点双的缩点就并不如边双那样直接了。由于一个割点可能存在于多个点双内,所以我们需要换一种方法,将割点单独视为一个点。这里直接给出代码,证明略(其实是因为我太菜了) 。

void tarjan(int x)
{
	dfn[x]=low[x]=++tot;
	sta[++top]=x;
	if(head[x]==0)
	{
		dcc[x]=++dc,v[dc].push_back(x);
		return ;
	}
	int cnt=0;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(!dfn[y])
		{
			tarjan(y);
			if(low[y]>=dfn[x])
			{
				++cnt;
				if(x!=rt||cnt>=2) cut[x]=1;
				++dc;
				while(1)
				{
					int z=sta[top--];
					dcc[z]=dc,v[dc].push_back(z);
					if(z==y) break;
				}
				dcc[x]=dc,v[dc].push_back(x);
			}
		}
		else low[x]=min(low[x],dfn[y]);
	}
}
int main()
{
	...
	int cur=dc;
	for(int i=1;i<=n;i++) if(cut[i]) dcc[i]=++cur;
	for(int i=1;i<=dc;i++)
	{
		for(int j=0;j<(int)v[i].size();i++)
		{
			int x=v[i][j];
			if(cut[x]) add_E(dcc[x],i),add_E(i,dcc[x]);
		}
	}
	for(int i=1;i<=m;i++)
	{
		int x=E[i].x,y=E[i].y;
		if(dcc[x]==dcc[y]) continue;
		add_E(dcc[x],dcc[y]),add_E(dcc[y],dcc[x]);
	}
}

性质:边双与点双缩完点后均会形成一棵树。

有时题目为了提升难度往往会在缩完点后的这棵树上做文章。

posted @ 2022-08-14 16:02  Freshair_qprt  阅读(795)  评论(0)    收藏  举报