xxx 无向图的双连通分量(Biconnected Components)
字数 2306 2025-11-08 10:02:38
xxx 无向图的双连通分量(Biconnected Components)
题目描述
在一个无向连通图中,我们称一个顶点为“割点”(Articulation Point),如果移除该顶点及其相连的边后,图会变得不再连通。而“双连通分量”则是一个极大的双连通子图。一个双连通子图意味着其中不包含任何割点,或者说,任意移除一个顶点,剩下的子图仍然是连通的。我们的目标是找出给定无向图的所有双连通分量。
解题过程
我们可以使用基于深度优先搜索(DFS)的算法来求解这个问题,它和寻找割点的算法紧密相关。核心思想是在DFS遍历的过程中,记录每个顶点的访问顺序(发现时间)和一个称为“low”的值,该值帮助我们发现割点并划分双连通分量。
步骤详解
-
基本定义与思路
- DFS编号(discovery time/disc):在DFS遍历时,我们为每个顶点分配一个唯一的、递增的编号,记录它被首次访问的顺序。
- Low值(low link):对于顶点u,
low[u]定义为以下值中的最小值:disc[u](自身)disc[v],对于所有与u在DFS树上有回边(back edge)相连的祖先v。low[w],对于所有u在DFS树上的直接子节点w。
- 核心观察:
low[u]实际上表示了从u出发,不经过DFS树中从根到u的路径上的第一条边,所能到达的最早(disc值最小)的祖先节点。如果某个子节点的low值不小于当前节点的disc值,那么这个当前节点很可能是一个割点,而栈中存储的边就构成了一个双连通分量。
-
算法准备
- 数据结构:
disc[]:数组,存储每个顶点的DFS发现时间。low[]:数组,存储每个顶点的low值。parent[]:数组,记录DFS树中每个顶点的父节点,用于避免将父节点误认为回边。stack:一个栈(Stack),用于在DFS过程中临时存储边。time:一个计数器,用于分配DFS发现时间。
- 初始化:将所有顶点的
disc和low初始化为-1(未访问),parent初始化为-1(无父节点)。time设置为0。
- 数据结构:
-
DFS遍历与关键步骤
我们从任意一个顶点开始DFS。对于当前访问的顶点u,我们执行以下操作:- 初始化u:将
disc[u]和low[u]都设置为当前time,然后time递增。 - 遍历邻居v:对于u的每一个邻居顶点v:
- 情况一:v未被访问过(即
disc[v] == -1)- 将边
(u, v)压入栈中。 - 设置
parent[v] = u。 - 递归地对v进行DFS。
- 回溯更新low值:当从v的递归调用返回后,更新
u的low值:low[u] = min(low[u], low[v])。 - 检查割点与输出分量:这是最关键的一步。如果v的
low[v] >= disc[u],那么u是一个割点(除非u是DFS树的根节点且有两个以上的子节点,这里我们通常单独判断根节点)。此时,u将图分成了两部分。我们从栈中不断弹出边,直到弹出边(u, v)为止。这些弹出的边就共同构成了一个双连通分量。
- 将边
- 情况二:v已被访问过,且v不是u的父节点(即
disc[v] != -1且v != parent[u])- 这表示我们找到了一条指向祖先v的回边。这条边
(u, v)也被压入栈中(注意,这里我们压入的是实际遍历时遇到的边,用于构建分量)。 - 更新u的low值:
low[u] = min(low[u], disc[v])。这里使用disc[v]而不是low[v],因为回边直接连接到了v。
- 这表示我们找到了一条指向祖先v的回边。这条边
- 情况一:v未被访问过(即
- 初始化u:将
-
处理根节点的特殊情况
DFS树的根节点是一个特例。判断根节点是否为割点的标准是看它在DFS树中是否有两个或以上的子节点。如果是,那么根节点也是一个割点。我们可以在DFS开始时,记录根节点的子节点数量,如果数量>=2,则在DFS结束后或发现第二个子节点时,将其标记为割点。在算法实现中,根节点的判断通常与“情况一”中的条件low[v] >= disc[u]结合处理。 -
算法总结与示例
- 输入:一个无向图G(通常用邻接表表示)。
- 过程:
- 初始化所有数组和栈。
- 对一个未访问的顶点调用DFS函数。
- 在DFS中,按照上述逻辑处理两种情况,并及时在发现割点时从栈中弹出边集以输出双连通分量。
- 输出:所有双连通分量,通常以边集列表的形式呈现。
举例说明
考虑一个简单的图:顶点为A, B, C, D,边为 (A-B), (B-C), (A-C), (C-D)。
- 假设从A开始DFS,访问顺序为 A->B->C->D。
- 当访问C时,会发现回边C-A(因为A已被访问且不是C的父节点B)。此时会更新C的low值。
- 当从D回溯到C时,检查
low[D] >= disc[C]? 假设D的low值等于其发现时间,且大于C的发现时间,条件成立。因此C是割点。此时栈顶到边(C-D)之间的边被弹出,组成一个双连通分量{C-D}。 - 继续回溯到B,检查
low[C] >= disc[B]? C的low值(因为回边指向A)小于B的发现时间,所以B不是割点。 - 回溯到A,检查根节点A。由于A有两个子节点(B和通过回边间接连接的C? 这里需要仔细分析DFS树),如果A在DFS树中确实有两个子节点,那么A是割点。我们会从栈中弹出剩余的边,这些边(A-B, B-C, C-A)构成了另一个双连通分量。
通过这个循序渐进的过程,算法能够高效地(时间复杂度O(V+E))找出图中所有的双连通分量。