概念解释
6ff254c8-c6a1-4800-a2a8-8308b82112a4
引理
若一张无向图不存在割点,则称它为“点双连通图”;
若一张无向图不存在割边,则称它为“边双联通图”。
边双连通分量(v-DCC)
第一定义:在一张连通的无向图中,对于两个点 和
,有两条边不重复的简单路径,那么这两个点边双联通。
第二(近似定义):无向图的极大边双联通子图被称为边双连通分量(内部不存在割边)
点双连通分量(e-DCC)
第一定义:在一张连通的无向图中,对于两个点 和
,有两条点不重复的简单路径,那么这两个点点双联通。
第二(近似定义):无向图的极大点双联通子图被称为点双连通分量(内部不存在割点)
说明
极大:我们称一个子图 极大,其中
,
等价于不存在包含该子图更大的子图
,使得
且
。
算法分析
e-DCC的判定
定理1
任意一条边都至少包含在一个简单环中(即无桥)
证明1
- 必要性:若 G 是边双连通图,假设存在桥 e,删除 e 后图不再连通,与边双连通定义矛盾,故 G 中无桥。
- 充分性:若 G 中无桥,则任意边 e 都在某个环中(否则 e 为桥)。此时删除任意边后,剩余边仍能通过环的另一半保持连通性,故 G 是边双连通图。
定理2
图中任意两点之间都存在两条边不相交的路径
证明2
- 必要性:边双连通图中,任意两点u 和 v 至少存在两条边不相交路径。若只有一条路径,则该路径上的边均为桥,与无桥矛盾。
- 充分性:若任意两点间存在两条边不相交路径,则删除任意一条边后,至少另一条路径仍连通,故图中无桥,满足边双连通定义
v-DCC的判定
定理1
极大性条件:子图是极大的不含割点的连通块
证明1
- 必要性:若子图 S 是v-DCC,假设存在割点 u,删除 u 后 S 不连通,与v-DCC定义矛盾,故 S 无割点。
- 充分性:若 S 是极大无割点连通块,则添加任意外部节点都会引入割点,因此 S 是极大点双连通子图。
定理2
路径条件:子图中任意两点间至少存在两条点不相交的路径(除端点外无公共点)
证明2
- 必要性:若 S 是v-DCC,任意两点 u,v 间存在两条点不相交路径。若路径相交于某点 w,则 w 为割点,与无割点矛盾。
- 充分性:若任意两点间存在两条点不相交路径,则删除任意一点后仍连通(因另一路径存在),故无割点,满足v-DCC定义。
定理3
- 任意两个不同的点双连通分量(v-DCC)最多只有一个公共点1。
- 若存在公共点,则该点一定是整个图的割点
证明3
先理解一下这个定理在讲什么。
- 唯一公共点:若两个 v-DCC 有公共点 u 和 v,则通过 u 和 v 可构造新路径使两分量连通,与 v-DCC 的极大性矛盾。
- 公共点为割点:删除该公共点后,两个 v-DCC 将断开连接(因无其他公共点),故其为割点
这个定理有一个推论:
- 非割点仅属一个 v-DCC,割点可属多个 v-DCC 。
我们再考虑证明。
(1) 唯一公共点(反证法)
- 假设:两个 v-DCC B1 和 B2 有两个公共点 u 和 v。
- 归纳奠基&矛盾推导:
- 由点双定义,B1 中 u,v 之间有两条点不相交路径 P1,P2;
- B2 中u,v 有两条点不相交路径 Q1,Q2;
- 通过 Pi 和 Qj 可构造 B1∪B2 的新路径,使 B1 和 B2 合并为更大点双连通子图,与极大性矛盾。
- 由此,推展到多个公共点,情况是一样的。
- 结论:公共点最多一个。
(2) 公共点为割点
- 假设:B1 和 B2 有唯一公共点 u。
- 证明步骤:
- 删除 u 后,B1 内部仍连通(因 B1 是点双);
- B2 内部仍连通;
- 但 B1 与 B2 之间无路径(因唯一公共点 u 被删除);
- 故图不再连通 → u 是割点。
定理4
- 分量根的身份:
v-DCC在DFS树中dfn最小的点要么是割点,要么是全局树根 - 割点作为分量根:
若该点是割点(非全局树根),则它必是当前v-DCC的根节点,因为其父节点属于其他分量 - 全局树根的特殊性:
- 子树≥2:是割点(删除后子树断开)13
- 子树=1:非割点,但作为v-DCC的根12
- 子树=0:孤立点自成v-DCC
证明4
情景1:分量根是割点(非全局树根)
- 逻辑:
设分量根为 u(非全局树根)。若 u 不是割点,则删除 u 后分量仍连通 → 与v-DCC定义矛盾(v-DCC是极大无割点子图)。 - 为何父节点不在分量中:
若父节点 p 属于该v-DCC,则 p 到 u 后代应有两条独立路径(绕过 u),但DFS树中p 到子树必须经过 u → 矛盾。
情景2:分量根是全局树根
- 子树≥2(割点):
删除根节点后,各子树间无路径(DFS树中子树无边)→ 图不连通 → 根是割点。
例:根A连接子树B、C,删A后B、C断开。 - 子树=1(非割点):
删除根节点后,唯一子树内部仍连通(因无边连向其他子树)→ 根非割点。
例:根A仅连子树B,删A后B仍连通。 - 子树=0(孤立点):
无删除目标 → 自成v-DCC。
参考程序
e-DCC
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N=4e6+5;
struct edge{int next,to;}e[N];
int head[N],idx=1;
int dfn[N],low[N],c,col[N],t;
int n,m;
bool bri[N];
vector<int> ans[N];
inline void add(int u,int v){ e[++idx]={head[u],v};head[u]=idx;}
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); }
while(c>='0'&&c<='9') { x=(x<<1)+(x<<3)+(c^48); c=getchar(); }
return x*f;
}
void dfs(int u)
{
col[u]=c;
ans[c].push_back(u);
for(int i=head[u];i;i=e[i].next)
if(!bri[i] && !col[e[i].to]) dfs(e[i].to);
}
void tarjan(int u,int last)
{
dfn[u]=low[u]=++t;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) bri[i]=bri[i^1]=true;
}
else if(i!=(last^1)) low[u]=min(low[u],dfn[v]);
}
}
int main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u,v; u=read(),v=read();
add(u,v),add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,-1);
for(int i=1;i<=n;i++)
if(!col[i])
++c,dfs(i);
cout<<c<<endl;
for(int i=1;i<=c;i++)
{
printf("%d ",ans[i].size());
for(auto j:ans[i])
printf("%d ",j);
}
return 0;
}
v-DCC
#include<iostream>
#include<stack>
#include<vector>
#include<cstdio>
using namespace std;
const int N=4e6+5;
struct edge{int next,to;}e[N];
int head[N],idx;
int dfn[N],low[N],c,t;
int n,m;
stack<int> s;
vector<int> ans[N];
int root;
bool cut[N];
inline int read()
{
int f=1,x=0;
char c=getchar();
while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); }
while(c>='0'&&c<='9') { x=(x<<1)+(x<<3)+(c^48); c=getchar(); }
return f*x;
}
inline void add(int u,int v){e[++idx]={head[u],v};head[u]=idx;}
void tarjan(int u)
{
dfn[u]=low[u]=++t;
s.push(u);
if(u==root && !head[u])
{
ans[++c].push_back(u);
return;
}
int child=0;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
child++;
tarjan(v);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v])
{
int p; c++;
do
{
p=s.top(); s.pop();
ans[c].push_back(p);
}
while(p!=v);
ans[c].push_back(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int main()
{
n=read(),m=read();
for(int i=1,u,v;i<=m;i++)
{
u=read(),v=read();
if(u==v) continue;
add(u,v),add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) root=i,tarjan(i);
printf("%d\n",c);
for(int i=1;i<=c;i++)
{
printf("%d ",ans[i].size());
for(int j:ans[i]) printf("%d ",j);
putchar('\n');
}
return 0;
}
细节实现
我们需要额外关注一下算法是如何实现的。tarjan教授是在是在图论领域建树卓越,笔者不得不详细地理解代码与数学定理之间的逻辑关系。以下讨论代码中的一些细节。
e-DCC
1.定理在代码上的体现
1. 定理核心逻辑的体现
(1) 桥的判定(tarjan
函数)
if(low[v] > dfn[u]) bri[i] = bri[i^1] = true; // 标记当前边及其反向边为桥
- 对应定理:通过
low[v] > dfn[u]
判断边(u,v)
是否为桥(即不在任何环中)。 - 逻辑:若子节点
v
无法通过非父子边回溯到u
或其祖先(low[v] > dfn[u]
),则(u,v)
是桥,符合定理中“桥不属于任何环”的性质。
(2) 边双连通分量的提取(dfs
函数)
if(!bri[i] && !col[e[i].to]) dfs(e[i].to); // 仅遍历非桥边
- 对应定理:通过忽略所有桥边(
!bri[i]
),确保每个连通块内部无边可删(即无桥),从而满足边双连通的定义。 - 逻辑:DFS遍历时跳过桥边,使得每个连通块内的边均属于至少一个环(即无桥),符合定理的“极大性”要求。
2. 代码流程与定理的映射
-
Tarjan算法:
- 计算
dfn
(访问时间戳)和low
(回溯最小值),动态标记桥边。 - 定理关联:桥的判定直接依赖“边是否在环中”(
low[v] > dfn[u]
时不在环中)。
- 计算
-
DFS划分e-DCC:
- 通过忽略桥边,将图分割为多个连通块,每个块内任意两点间存在两条边不相交路径(因无桥)37。
- 定理关联:连通块的划分结果即为边双连通分量,满足“极大无桥子图”的定义。
总结
- 桥的判定 → 体现“边不在环中则为桥”的定理。
- DFS忽略桥边 → 确保每个分量内部无桥,符合“边双连通分量是极大无桥子图”的定理。
- 输出结果 → 每个
ans[i]
存储一个e-DCC,其内部边均属于至少一个环。
2.e-DCC缩点的意义与实现逻辑
1. 缩点的意义
缩点将原图中每个边双连通分量(e-DCC) 压缩为单一节点,并用桥(割边) 连接这些节点。其核心意义在于:
- 简化图结构:消除分量内部冗余的环结构,仅保留分量间的关键连接(桥)。
- 转化为树/森林:缩点后的图成为无环连通图(树或森林),便于分析全局连通性。
2. 逻辑实现步骤
(1) 标记桥边
通过Tarjan算法判定桥边(核心代码):
void tarjan(int u, int last) {
dfn[u] = low[u] = ++t;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (!dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) // 桥判定条件
bri[i] = bri[i^1] = true; // 标记桥及其反向边
}
else if (i != (last^1))
low[u] = min(low[u], dfn[v]);
}
}
-
定理体现:
low[v] > dfn[u]
表明边(u,v)
不在任何环中(即桥)23。
(2) DFS划分e-DCC并缩点
忽略桥边进行DFS,划分连通块(缩点核心):
void dfs(int u) {
col[u] = c; // 为节点分配e-DCC编号
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (!bri[i] && !col[v]) // 忽略桥边
dfs(v);
}
}
- 缩点操作:
- 每个e-DCC分配唯一编号(如
c[u]
)。 - 遍历原图所有桥边
(u,v)
,在缩点图中添加新边(c[u], c[v])
12。
- 每个e-DCC分配唯一编号(如
3. 缩点后的图状态
- 拓扑结构:缩点图是树或森林(若原图不连通)。
- 关键性质:
- 节点内部无桥(e-DCC性质)。
- 节点间的边均为原图的桥。
- 任意两点间存在唯一路径(树的性质)。
4. 缩点的应用价值
-
简化连通性分析:在树结构上可直接用LCA等算法处理路径问题。
-
网络容错设计:桥边是网络脆弱点,缩点后便于识别关键连接。
-
算法优化基础:为后续的环检测、动态连通性问题提供高效框架。
总结
e-DCC缩点通过压缩环结构为单点、保留桥为边,将复杂图转化为树结构,既保留了关键连通信息,又极大简化了图论问题的处理难度
v-DCC
1.定理在代码上的体现
整体框架是割点的框架,所以很多细节不再赘述,可以参考笔者的关于割点&割边的文章。
1. 孤立点处理(if(x==root&&head[x]==0)
)
- 作用:单独处理孤立节点(无邻边的节点),将其作为独立的v-DCC。
- 逻辑:若当前节点是根节点且无邻边(
head[x]==0
),直接存入dcc
数组并返回。
2. 子节点遍历(for
循环)
- 作用:遍历当前节点
x
的所有邻接节点y
,递归处理未访问节点。 - 关键逻辑:
- 未访问节点(
!dfn[y]
):递归调用tarjan(y)
,更新low[x]
,并检查割点条件。 - 已访问节点(
else
):直接通过dfn[y]
更新low[x]
(无需考虑父节点或重边)。
- 未访问节点(
3. 割点判定与v-DCC提取(if(dfn[x]<=low[y])
)
- 割点判定:
- 若
dfn[x] <= low[y]
,说明x
是割点(y
无法绕过x
回溯)。 - 根节点特判:根节点需至少两个子节点(
child > 1
)才标记为割点。
- 若
- v-DCC提取:
- 弹出栈中节点直到
y
,构成一个v-DCC,并将割点x
加入该分量(割点属于多个v-DCC)。
- 弹出栈中节点直到
4. 逻辑关系总结
- 孤立点优先处理 → 避免无效遍历。
- 递归子节点 → 确保深度优先搜索(DFS)顺序。
- 割点判定与分量提取 → 依赖子节点的
low
值回溯,动态维护栈结构。
2.v-DCC缩点如何实现
总结归纳
事实上,本文很多细节在前文论述割点、割边时已然揭晓,所以理解起来没有前面那么困难。因为DCC的研究就是基于割点和割边的,所以研究清楚了割点与割边,这里就不会很困难。再次惊叹于tarjan教授的雄才伟略。
最后,我们对比一下重边,自环对割顶,割边,e-DCC,v-DCC的影响及处理方法。
重边与自环对连通性算法的影响及处理方案
一、重边(平行边)的影响与处理
这里其实要说一句,可以在输入的时候直接跳过,这样统一避免影响。
-
割边(桥)
- 影响:重边会导致一条边被误判为桥。例如,若两点间存在两条平行边,删除其中一条仍连通,因此该边实际不是桥。
- 处理:
- 在Tarjan算法中,判断桥的条件
low[v] > dfn[u]
需修正为 low[v] > dfn[u]
且该边非重边。 - 实现时记录边的唯一编号,遍历邻接边时跳过重复边。
- 在Tarjan算法中,判断桥的条件
-
边双连通分量(e-DCC)
- 影响:重边可能使本属同一e-DCC的子图被错误分割(如两点间重边本应使子图无边双割裂)。
- 处理:
- 缩点时将重边视为单一边,确保e-DCC内部无边连通。
- 使用边判重机制(如哈希边
(min(u,v), max(u,v))
)避免重复处理。
-
割点与点双连通分量(v-DCC)
- 影响:基本无影响。因割点判定依赖点的删除而非单一边,v-DCC定义本身不关注重边34。
- 处理:无需特殊处理。
二、自环的影响与处理
-
割边(桥)
- 影响:自环不参与连通性,删除自环后图连通性不变,因此自环不可能是桥。
- 处理:在DFS前预处理删除所有自环,或在遍历邻接点时跳过
u == v
的边。
-
边双连通分量(e-DCC)
- 影响:自环会导致单个点被错误识别为一个e-DCC(如孤立点带自环)。
- 处理:
- 预处理删除自环,确保e-DCC至少包含两个点(除非是孤立点)。
- DFS中忽略自环边,避免将其计入连通路径。
-
割点
- 影响:无直接影响。删除自环关联的点时,自环自动失效,不影响割点判定34。
- 处理:无需调整算法。
-
点双连通分量(v-DCC)
- 影响:自环可能干扰v-DCC的构造(如自环点被误判为割点)。
- 处理:
- 删除自环后,孤立点视为独立v-DCC;非孤立点按正常流程计算。
- 在点双栈中忽略自环点,确保分量至少包含两个点(除非是孤立点)。
三、总结与通用处理建议
要素 | 重边 | 自环 |
---|---|---|
割边 | 检查非重边才判桥 | 直接删除 |
e-DCC | 缩点时视重边为单一边 | 删除后处理,避免单点分量 |
割点 | 无影响 | 无影响 |
v-DCC | 无影响 | 删除后处理,避免假环 |
算法实现关键:
- 预处理:移除所有自环边,对重边进行合并或标记。
- Tarjan修正:
- 割边判定增加重边检查(
if (edge_id != reverse_edge_id)
)。 - 点双/边双DFS跳过自环邻接点(
if (v == u) continue;
)。
- 割边判定增加重边检查(
- 缩点后处理:确保e-DCC和v-DCC不包含无效单点(除孤立点外)
还是非常有意思的,图论!