点双、边双、强联通分量整理

定义

首先要明确各种定义,非常容易搞混:

  1. 时间戳:数组一般叫 d f n [    ] dfn[\;] dfn[],记录点第一次入栈的顺序。
  2. 搜索树:dfs 时生成的树
  3. 割点:去掉这个点,原无向联通图不再联通。割点集合同理。
  4. 割边/桥:去掉这条边,原无向联通图不再联通。
  5. 点双联通图:不存在割点的图。判定:顶点数不超过2的无向联通图是点双。顶点数大于2的无向连通图是点双,当且仅当任意两点至少包含在一个简单环内
  6. 边双联通图:不存在割边的图。判定:任意一条边包含在一个环内
  7. 点双联通分量:极大点双联通图,即不存在包含这个点双联通子图的更大的双联通子图。
  8. 边双联通分量:同6,点改为边。
  9. 边双缩点:去掉割边,一个联通块缩成一个点。
  10. 点双缩点:点双缩成一个点,与割点连成图。
  11. 强连通分量的树边、前向边、后向边、横插边:1搜索树上的边,2连到子孙,3连到祖先,4连到不在同一棵子树的点。

Tarjan算法


割点和点双

无向图 的双连通分量(biconnectivity)

  1. 定义:
    • articulation point: 关节点,去掉这个点图变得不连通
    • biconnected graph: 不存在关节点
    • biconnected component: maximal biconnected subgraph
  2. tarjan 算法:
    1. 生成一颗 dfs 树,遍历顺序记为 dfn[v]
    2. 除了树边之外仅可能存在一种边:连接 u 和 u 子树中的节点 v 的边。记 low[v] 为从 v 和 v 子树中的节点 出发走 1 条 非树边能够到达的最小 dfn。当 u 的某个儿子 v low[v]>=dfn[u] 时 u 是关节点。
    3. 想要记录每个双联通分量中的点有哪些:将遍历到的点都入栈,在找到关节点的时候不断出栈直到关节点出栈,然后再把关节点入栈。(因为每个关节点可能会被包含在多个点双中)
    4. 注意:对于图的所有不连通的分量都要搜索,注意特判孤立节点的情况
void Tarjan(int u)
{
    dfn[u] = low[u] = ++idx;
    if (rt == u && point[u] == -1){ // 孤立的点特判
        dccn++;
        dcc[dccn].clear();
        dcc[dccn].push_back(u);
        return;
    }
    stk[++st] = u;
    for (int i = point[u]; i != -1; i = edge[i].nxt){
        int v = edge[i].v;
        if (!dfn[v]){
            Tarjan(v);
            low[u] = min(low[u], low[v]);
            // 因为v在u的子树内,所以low[v]可以用于更新low[u]
            if (low[v] >= dfn[u]){ // 子树中没有可以连到父亲上面的边
            	cut[u] = 1;
                dccn++;
                dcc[dccn].clear();
                dcc[dccn].push_back(u); // u是割点,可能包含在多个点双中,不能弹出
                while (1){
                    int w = stk[st];
                    dcc[dccn].push_back(w);
                    st--;
                    if (w == v){ // 做到子树全部弹出为止,不然v的兄弟也会被弹出
                        break;
                    }
                }
            }
        }
        else{
            low[u] = min(low[u], dfn[v]);
            // low表示u的子树中的非树边能到的最小的dfn值,所以不能和low[v]比较,详见下图
        }
    }
}

看这样一张图:(图中标出的数字是 dfn[u],对于节点 3,他会先找到 1 并更新 low[3] 然后再拓展 4 号节点)
在这里插入图片描述
如果 e l s e else else时用 l o w [ v ] low[v] low[v]更新 l o w [ u ] low[u] low[u],这张图就是一整个点双了。然而并不是,3明显是一个割点。

割边和边双
void Tarjan(int u, int in_e)
{
	dfn[u] = ++idx;
	low[u] = u;
	for (int i = point[u]; i != -1; i = edge[i].nxt){
		if (i == in_e){
			continue;
		}
		int v = edge[i].v;
		if (!dfn[v]){
			Tarjan(v, i^1);
			low[u] = min(low[u], low[v]);
			if (low[v] < dfn[u]){
				cut_e[i] = cut_e[i^1] = 1;
			}
		}
		else{
			low[u] = min(low[u], dfn[v]);
			// 这里写dfn[v]或者low[v]没什么关系,因为v能通过非树边到达的点u肯定也可以
			// low[]甚至可以看成一个类似并查基集的结构
		}
	}
}

求边双只要求出割边之后 d f s dfs dfs一遍就行了。

强联通分量

p.s. 点双和边双是针对无向图而言,强联通分量是针对有向图而言。因为是有向图所以才存在前向边后向边的说法。

强连通分量的算法是找后向边和横插边构成的环。在栈中记录当前节点的祖先,和能到达当前节点祖先的点。如果有边指向栈中节点,那么就可以用栈中节点的 d f n dfn dfn更新当前节点的 l o w low low

void Tarjan(int u)
{
    dfn[u] = low[u] = ++idx;
    stk[++st] = u;
    instk[u] = 1;
    for (int i = point[u]; i != -1; i = edge[i].nxt){
        int v = edge[i].v;
        if (!dfn[v]){
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if (instk[v]){
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (low[u] == dfn[u]){
    // 如果u不能到达他的祖先,那么在回溯之后他就不满足存在栈中的条件了
        cnum++;
        while (1){
            int v = stk[st--];
            col[v] = cnum;
            instk[v] = 0;
            scc[cnum].push_back(v); // scc中记录每一个强联通分量
            if (v == u){
                break;
            }
        }
    }
}

例题

一、poj 2942
Knights of the Round Table
点双+奇环判定


二、poj 3694
Network
边双,low[]的并查集用法


  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值