专题 无向图的双连通分量

概念解释

6ff254c8-c6a1-4800-a2a8-8308b82112a4

引理

        若一张无向图不存在割点,则称它为“点双连通图”;

        若一张无向图不存在割边,则称它为“边双联通图”。

边双连通分量(v-DCC)

        第一定义:在一张连通的无向图中,对于两个点 u 和 v ,有两条边不重复的简单路径,那么这两个点边双联通。

        第二(近似定义):无向图的极大边双联通子图被称为边双连通分量(内部不存在割边)

点双连通分量(e-DCC)

        第一定义:在一张连通的无向图中,对于两个点 u 和 v ,有两条点不重复的简单路径,那么这两个点点双联通。

        第二(近似定义):无向图的极大点双联通子图被称为点双连通分量(内部不存在割点)

说明

        极大:我们称一个子图 G{}'=(V{}',E{}') 极大,其中 V{}'\subseteq VE{}'\subseteq E 等价于不存在包含该子图更大的子图 G{}''=(V{}'',E{}'') ,使得 V{}'\subseteq V{}'' \subseteq V且 E{}'\subseteq E{}'' \subseteq E

算法分析

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‌。
  • 证明步骤‌:
    1. 删除 u 后,B1​ 内部仍连通(因 B1​ 是点双);
    2. B2​ 内部仍连通;
    3. 但 B1​ 与 B2​ 之间无路径(因唯一公共点 u 被删除);
    4. 故图不再连通 → 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. 代码流程与定理的映射
    1. Tarjan算法‌:

      • 计算 dfn(访问时间戳)和 low(回溯最小值),动态标记桥边。
      • 定理关联‌:桥的判定直接依赖“边是否在环中”(low[v] > dfn[u] 时不在环中)。
    2. 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。
    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. 逻辑关系总结
    1. 孤立点优先处理‌ → 避免无效遍历。
    2. 递归子节点‌ → 确保深度优先搜索(DFS)顺序。
    3. 割点判定与分量提取‌ → 依赖子节点的 low 值回溯,动态维护栈结构。

    2.v-DCC缩点如何实现

    总结归纳

            事实上,本文很多细节在前文论述割点、割边时已然揭晓,所以理解起来没有前面那么困难。因为DCC的研究就是基于割点和割边的,所以研究清楚了割点与割边,这里就不会很困难。再次惊叹于tarjan教授的雄才伟略。

            最后,我们对比一下重边,自环对割顶,割边,e-DCC,v-DCC的影响及处理方法。

    重边与自环对连通性算法的影响及处理方案

    一、重边(平行边)的影响与处理

            这里其实要说一句,可以在输入的时候直接跳过,这样统一避免影响。‌

    1. 割边(桥)

      • 影响‌:重边会导致一条边被误判为桥。例如,若两点间存在两条平行边,删除其中一条仍连通,因此该边实际不是桥。
      • 处理‌:
        • 在Tarjan算法中,判断桥的条件 low[v] > dfn[u] 需修正为 ‌low[v] > dfn[u] 且该边非重边‌‌。
        • 实现时记录边的唯一编号,遍历邻接边时跳过重复边。
    2. 边双连通分量(e-DCC)

      • 影响‌:重边可能使本属同一e-DCC的子图被错误分割(如两点间重边本应使子图无边双割裂)。
      • 处理‌:
        • 缩点时将重边视为单一边,确保e-DCC内部无边连通‌。
        • 使用边判重机制(如哈希边(min(u,v), max(u,v)))避免重复处理‌。
    3. 割点与点双连通分量(v-DCC)

      • 影响‌:‌基本无影响‌。因割点判定依赖点的删除而非单一边,v-DCC定义本身不关注重边‌34。
      • 处理‌:无需特殊处理。
    二、自环的影响与处理
    1. 割边(桥)

      • 影响‌:自环不参与连通性,删除自环后图连通性不变,因此自环‌不可能是桥‌‌。
      • 处理‌:在DFS前预处理删除所有自环,或在遍历邻接点时跳过 u == v 的边‌。
    2. 边双连通分量(e-DCC)

      • 影响‌:自环会导致单个点被错误识别为一个e-DCC(如孤立点带自环)。
      • 处理‌:
        • 预处理删除自环,确保e-DCC至少包含两个点(除非是孤立点)。
        • DFS中忽略自环边,避免将其计入连通路径‌。
    3. 割点

      • 影响‌:‌无直接影响‌。删除自环关联的点时,自环自动失效,不影响割点判定‌34。
      • 处理‌:无需调整算法。
    4. 点双连通分量(v-DCC)

      • 影响‌:自环可能干扰v-DCC的构造(如自环点被误判为割点)。
      • 处理‌:
        • 删除自环后,孤立点视为独立v-DCC;非孤立点按正常流程计算‌。
        • 在点双栈中忽略自环点,确保分量至少包含两个点(除非是孤立点)‌。
    三、总结与通用处理建议
    要素重边自环
    割边检查非重边才判桥直接删除
    e-DCC缩点时视重边为单一边删除后处理,避免单点分量
    割点无影响无影响
    v-DCC无影响删除后处理,避免假环

    算法实现关键‌:

    1. 预处理‌:移除所有自环边,对重边进行合并或标记‌。
    2. Tarjan修正‌:
      • 割边判定增加重边检查(if (edge_id != reverse_edge_id))‌。
      • 点双/边双DFS跳过自环邻接点(if (v == u) continue;)‌。
    3. 缩点后处理‌:确保e-DCC和v-DCC不包含无效单点(除孤立点外)

    还是非常有意思的,图论!

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值