【图论】总结 8:边双连通分量

在有向图中有强连通分量(SCC)这一概念,类似地在无向图中有边双连通分量(e-DCC)和点双连通分量(v-DCC)的概念。


无向图的割边与 e-DCC

对于一张连通的无向图,如果我们将其中一条边删去,将会将无向图分裂为两个连通块,那么我们称这条边为无向图的割边。例如下图展示了一个无向图的所有割边(红粗边)以及分裂后形成的若干连通块:

我们可以将每个连通块想象成一块块“陆地”,那么割边好像连接这些陆地的“桥”,因此无向图的割边也称作

而对于一个连通图,如果其不存在割边,我们称其是边双连通图。无向图中极大的边双连通子图被称为无向图的边双连通分量(e-DCC)。

我们注意到对于一张无向图,将其割边全部删去后,剩余的每个连通块就是该无向图的 e-DCC。

Tarjan 算法求割边

类似于有向图中求 SCC 的过程,我们定义每个节点 \(u\)时间戳 \(dfn[u]\)追溯值 \(low[u]\)

\(dfn[u]\):对于无向图,在 DFS 形成搜索树的过程中按照节点第一次被遍历到的先后顺序给节点赋予 \(1\sim n\) 的整数标记,该标记即为 \(dfn[u]\)
\(low[u]\):是满足以下任意一条的所有节点中时间戳的最小值:(1)搜索树上 \(\text{SubTree}(u)\) 中的节点;(2)通过 \(1\) 条不在搜索树上的边,能到达 \(\text{SubTree}(u)\) 上的节点。

下图展示了一张无向图中每个节点的时间戳与追溯值(节点内为时间戳,蓝色数字为追溯值,绿色粗边为树枝边):

注意,无向图中是不存在类似有向图中的“横叉边”的,因为如果存在这样的边,我们是可以遍历之使其成为树枝边的。

我们可以发现,无向图的割边一定是搜索树上的树枝边,且割边一定不属于一个简单环。

利用 \(dfn\) 数组以及 \(low\) 数组,我们可以得出:

无向边 \((u,v)\) 是割边,当且仅当搜索树上存在 \(u\) 的一个子节点 \(v\) 满足 \(dfn[u]<low[v]\)

Tarjan 算法利用了这个结论,可在 \(O(n+m)\) 的时间复杂度内求得无向图的所有割边。我们以上面的例子过一遍 Tarjan 算法的流程。

初始时,有一个栈 \(s\) 为空。我们首先遍历到 \(1\) 号节点,此时 idx ++\(idx=1\))并赋值给 \(dfn[1]\)\(low[1]\),将 \(1\) 入栈:

接下来遍历到 \(2\) 号节点:此时 idx ++\(idx=2\))并赋值给 \(dfn[2]\)\(low[2]\),将 \(2\) 入栈。

以此类推遍历到 \(5\) 号节点:此时 idx ++\(idx=5\))并赋值给 \(dfn[5]\)\(low[5]\),将 \(5\) 入栈,此时情景如下图:

接下来我们遍历到 \(1\) 号节点。此时我们发现 \(1\) 号节点已被遍历,所以令 \(low[5]=\min\{low[5],dfn[1]\}=1\),这一步与有向图 Tarjan 算法求 SCC 的一步很类似。

同理继续扫描 \(5\) 号节点的其它出边,遍历到 \(2\),令 \(low[5]=\min\{low[5],dfn[2]\}=1\)

\(5\) 号节点没有出边了,回溯到 \(4\),更新 \(low[4]=\min\{low[4],low[5]\}=1\)

继续回溯到 \(3\),更新 \(low[3]=\min\{low[3],low[4]\}=1\)

继续回溯到 \(2\),更新 \(low[2]=\min\{low[2],low[3]\}=1\)

继续回溯到 \(1\),发现 \(low[1]=dfn[1]\),因此 \(1\) 为该边双连通分量的根(可类似于 SCC 的根理解),并不断弹栈直到 \(1\) 被弹出,被弹出的元素属于同一个 e-DCC,我们记录并标记颜色:

接下来继续扫描 \(1\) 的出边,\(5\) 已被遍历,所以我们跳过。我们访问到 \(6\) 号节点,此时 idx ++\(idx=6\))并赋值给 \(dfn[6]\)\(low[6]\),将 \(6\) 入栈:

接下来遍历到 \(7\),此时 idx ++\(idx=7\))并赋值给 \(dfn[7]\)\(low[7]\),将 \(7\) 入栈。

以此类推遍历到 \(9\),此时 idx ++\(idx=9\))并赋值给 \(dfn[9]\)\(low[9]\),将 \(9\) 入栈,场景如下:

接下来我们遍历到 \(7\) 并发现 \(7\) 已被遍历,所以令 \(low[9]=\min\{low[9],dfn[7]\}=7\)

\(9\) 没有其它出边了,回溯到 \(8\),更新 \(low[8]=\min\{low[8],low[9]\}=7\)

继续回溯到 \(7\),发现 \(low[7]=dfn[7]\),因此 \(7\) 为该 e-DCC 的根,不断弹栈直到 \(7\) 被弹出,被弹出的元素同属于一个 e-DCC,我们记录并标记颜色:

继续回溯到 \(6\),令 \(low[6]=\min\{low[6],low[7]\}=6\) 此时我们发现 \(dfn[6]<low[7]\),说明边 \((6,7)\) 为割边,我们打上标记。然后发现 \(low[6]=dfn[6]\),说明 \(6\) 为该 e-DCC 的根,我们不断弹栈直到 \(6\) 被弹出,记录该 e-DCC 并标记颜色:

同理回溯到 \(1\) 时有 \(dfn[1]<low[6]\),我们标记 \((1,6)\) 为割边:

如果无向图不连通,Tarjan 算法将会在其余连通块上进行类似操作。

Tarjan 算法的代码实现

为方便起见,我们用邻接表存图而非 vector,这是因为我们在存图时是将一条无向边视为两条逆向的有向边的,利用位运算异或成对变换的性质(例如 \(2\oplus 1=3,3\oplus 1=2\)),我们可以很方便的处理一些细节问题。

void tarjan(int u, int from)//from 为来访边的编号
{
	dfn[u] = low[u] = ++ idx;
	s.push(u);
	for(int i = h[u]; ~i; i = ne[i])
	{
		int v = e[i];
		if(i == (from ^ 1)) continue;//如果跑回去,直接跳过
		if(!dfn[v])
		{
			tarjan(v, i);
			low[u] = min(low[u], low[v]);
			if(dfn[u] < low[v])//找到割边
				bridge[i] = bridge[i ^ 1] = true;//该边及其逆向边均标记
		}
		else low[u] = min(low[u], dfn[v]);
	}
	if(low[u] == dfn[u])//记录 e-DCC
	{
		vector<int> res;
		int top;
		eDCC ++;
		do
		{
			top = s.top();
			s.pop();
			color[top] = eDCC;//颜色(编号)
			res.push_back(top);//记录具体节点
		}while(top != u);
		ans.push_back(res);
	}
}

例如对于 P8436 【模板】边双连通分量,参考代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10, M = 4e6 + 10;
int n, m;
int h[N], e[M], ne[M], ide;
void add(int u, int v)//邻接表存图
{
	e[ide] = v, ne[ide] = h[u], h[u] = ide ++;
}
int dfn[N], low[N], idx;
stack<int> s;
bool bridge[M];
vector<vector<int> > ans;
int eDCC, color[N];
void tarjan(int u, int from)
{
	dfn[u] = low[u] = ++ idx;
	s.push(u);
	for(int i = h[u]; ~i; i = ne[i])
	{
		int v = e[i];
		if(i == (from ^ 1)) continue;
		if(!dfn[v])
		{
			tarjan(v, i);
			low[u] = min(low[u], low[v]);
			if(dfn[u] < low[v])
				bridge[i] = bridge[i ^ 1] = true;
		}
		else low[u] = min(low[u], dfn[v]);
	}
	if(low[u] == dfn[u])
	{
		vector<int> res;
		int top;
		eDCC ++;
		do
		{
			top = s.top();
			s.pop();
			color[top] = eDCC;
			res.push_back(top);
		}while(top != u);
		ans.push_back(res);
	}
}
int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m;
	for(int i = 1; i <= m; i ++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		add(u, v), add(v, u);
	}
	for(int i = 1; i <= n; i ++)
		if(!dfn[i]) tarjan(i, -1);
	printf("%d\n", eDCC);
	for(auto i : ans)
	{
		printf("%d ", i.size());
		for(auto j : i) printf("%d ", j);
		puts("");
	}
	return 0;
}

e-DCC 的缩点

将每个 e-DCC 视为一个“大点”,将割边视为连接“大点”的边,那么将会将无向图变成一棵树(森林):

我们在 Tarjan 算法的过程中记录颜色数组 \(color\),然后扫描每条边,如果每条边的两个端点的颜色不同,说明它们属于不同的 e-DCC,这条边即为割边,我们以颜色(编号)代表每个“大点”编号,然后连边即可。

for(int i = 0; i < ide; i ++)
	if(color[e[i ^ 1]] != color[e[i]])
		e2[color[e[i ^ 1]]].push_back(color[e[i]]);
posted @ 2025-07-25 11:26  cold_jelly  阅读(39)  评论(0)    收藏  举报