跳转至

双连通分量

边双联通分量 e-DCCψ(`∇´)ψ

割边/桥:如果在无向图 \(G=(V,E)\) 当中去掉一条边 \((u,v)\) 后,\(G\) 分裂为两个不联通的子图,则称边 \((u,v)\) 是无向图 \(G\) 的一个桥。

边双联通分量:不含桥边的极大联通子图。

类似 SCC,引入 \(G\) 的一颗搜索树 \(T\)\(dfn,low\)

\(low\) 的定义也是从某个节点向上最高(在 \(T\) 当中)能到达的节点的 \(dfn\)

那么要找到桥边,实际上就是找到搜索树上相邻的两个节点 \((u,v)\) ,使得 \(u\)\(v\) 的父亲且 \(dfn_u<low_v\),也就是 \(v\) 无论如何都没有办法走到 \(u\) 或者 \(u\) 更上面的节点。

这是一个无向图,所以 \(v\)\(v\) 的子树都是没有边能到达 \(u\) 以及更上面的子图的。

因为无向图的 dfs 树以外的边都不是横叉边,所以只需要考虑“回边”的影响。

那么必然可以证明,\((u,v)\) 一定是桥边。

这里也可以同时得到一个性质,桥边一定是树边

而且,任意一个简单环中的边都不是桥边

去掉桥边之后,剩下的连通块就是一个个 e-DCC。

缩点也比较容易,直接把所有去掉桥边之后的连通块分别合并成一个节点即可。

Code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int si = 2e5 + 10;

int n, m, q;

// 原图
int head[si], tot1 = 0;
struct Edge {
    int ver, Next;
}e[si << 2]; 
inline void add1(int u, int v) {
    e[tot1] = (Edge){v, head[u]}, head[u] = tot1++;
}

// 缩完点之后的图
// 如果原来的图是连通图的话
// 可以证明缩完点之后必然是一棵树。
int Head[si], tot2 = 0;
struct Tree {
    int ver, Next;
}t[si << 2];
inline void add2(int u, int v) {
    t[tot2] = (Tree){v, Head[u]}, Head[u] = tot2++;
}

// E-dcc 的个数.
int cnt = 0; 

int dfn[si], low[si], tim = 0;
// 是否是桥
bool bridge[si << 2];
int c[si];

// in_edge 是用来消除重边的影响的。
// 表示当前状态是从哪一条边过来的。
void tarjan(int u, int in_edge) {
    dfn[u] = low[u] = ++tim;
    for(int i = head[u]; ~i; i = e[i].Next) {
        int v = e[i].ver;
        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 if((i ^ 1) != in_edge) low[u] = min(low[u], dfn[v]);
    }
}

// 去掉桥边的连通块染色
void dfs(int u, int col) {
    c[u] = col;
    for(int i = head[u]; ~i; i = e[i].Next) {
        int v = e[i].ver;
        if(c[v] || bridge[i]) continue;
        dfs(v, col);
    }
} 
void Construct() {
    for(int i = 1; i <= n; ++i){
        for(int j = head[i]; ~j; j = e[j].Next) {
            int v = e[j].ver;
            if(c[i] == c[v]) continue;
            // 只需要加一次,遍历到反向边的时候会自动补全成无向边
            add2(c[i], c[v]);
        }
    }
}

int main() {
    memset(head, -1, sizeof head);
    memset(Head, -1, sizeof Head);
    memset(bridge, false, sizeof bridge);
    cin >> n >> m;
    for(int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        add1(u, v), add1(v, u);
    }
    for(int i = 1; i <= n; ++i) if(!dfn[i]) tarjan(i, -1);
    for(int i = 1; i <= n; ++i) if(!c[i]) ++cnt, dfs(i, cnt);
    Construct();
}

\(in\_edge\) 表示递归到当前节点所经过的那一条边的编号。

\(in\_edge\) 存在的原因是,如果按照正常搜索树的更新方式,\(fa_u\)\(dfn\) 必然不会用来更新 \(u\)\(low\)

但是如果出现重边的话,那 \((fa_u,u)\) 必然不是桥边,那么就需要用 \(fa_u\)\(dfn\) 更新 \(low_u\)

可以证明的一个推论:对于一个无向连通图,Edcc 缩完点之后必然会形成一棵树(这东西被叫做边双树来着)。

因为只要图上带环,就可以继续缩点,而树是无向连通无环图,所以得证。

点双联通分量 v-DCCψ(`∇´)ψ

割点:如果对于无向图 \(G=(V,E)\)\(\exist x \in V\),使得删除 \(x\) 之后,\(G\) 分裂成两个及以上的不联通的子图,则称节点 \(x\) 是无向图 \(G\) 的一个割点。

点双连通分量:不含割点的极大联通子图,但是注意,这里不能直接求出割点之后去掉割点把连通块合并

因为割点本身就属于它连接的点双联通分量(同时属于)。

所以要特别注意。

仍然引入 \(T\)\(dfn\)\(low\)

考虑一个节点 \(u\) 怎么才可能成为割点。

如果存在一条树边 \((u,v)\)\(u\)\(v\) 的父亲节点,且满足 \(dfn_u \le low_v\)\(u\) 不是根节点。

如果 \(u\) 是根节点,那么需要满足两次以上 \(dfn_u \le low_v\)

也就是 \(v\)\(v\) 的子树里的节点最高只能到 \(u\) ,到不了 \(u\) 更上层的节点,那么 \(u\) 必然会把 \(v\)\(v\) 的子树以及 \(u\) 上面的子图分开。

所以此时 \(u\) 就是一个割点。

递归的同时维护一个栈,原理类似 SCC。

然后每次满足 \(dfn_u \le low_v\) 的时候就弹出,直到 \(v\) 出栈,被弹出的节点就组成一个 v-DCC。

(这里不需要判根,可以自己画图)

不要把 \(u\) 弹出去了,如果 \(u\) 是割点,那么之后访问到的 v-DCC 里面就会少 \(u\) 这个点。

因为判定条件里面有 \(=\),所以就不需要再判 \(in\_edge\) 了,不过在没有特殊说明的情况下,一定要记住把重边直接在读入的时候判掉。

缩点的话唯一要注意的就是要把割点单独分裂出来,但是割点又要存在于它连接的所有v-DCC 当中。

所以要给割点一个新编号。

Code
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <stack>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int si = 1e5 + 10;

int n, m, root;

// 原图
int head[si], tot1 = 0;
// 新图
int Head[si], tot2 = 0;
struct Edge {
    int ver, Next;
}e[si << 2], g[si << 2];
inline void add1(int u, int v) {
    e[tot1] = (Edge){v, head[u]}, head[u] = tot1++;
}
inline void add2(int u, int v) {
    g[tot2] = (Edge){v, Head[u]}, Head[u] = tot2++;
}


// Vdcc 的个数
int cnt = 0;
int dfn[si], low[si];
int c[si], tim;
// 割点的新编号
int new_id[si];
// 是否是割点
bool cut[si];
stack<int> s;
vector<int> vdcc[si];

void tarjan(int u) {
    dfn[u] = low[u] = ++tim;
    s.push(u);

    // 孤立点
    if(u == root && head[u] == -1) {
        vdcc[++cnt].emplace_back(u);
        return;
    }

    int flag = 0;
    for(int i = head[u]; ~i; i = e[i].Next) {
        int v = e[i].ver;
        if(!dfn[v]) {
            tarjan(v), low[u] = min(low[u], low[v]);

            if(dfn[u] <= low[v]) {
                ++flag;
                // 根节点特判
                if(u != root || flag > 1) { // 注意这里是短路运算符,不要打反了。
                    cut[u] = true;
                }
                int x; ++cnt;
                do {
                    x = s.top(), s.pop();
                    vdcc[cnt].emplace_back(x);
                } while(v != x);
                // 注意这里要是 v 不是 u
                // 如果 u 被弹出了,之后的连通块就会少 u。
                vdcc[cnt].emplace_back(u);
            }
        }
        else low[u] = min(low[u], dfn[v]);
    }
}

int num;
void Construct() {
    num = cnt;
    for(int u = 1; u <= n; ++u) {
        if(cut[u]) new_id[u] = ++num;
    }

    for(int i = 1; i <= cnt; ++i) {
        for(int j : vdcc[i]) {
            if(cut[j]) add2(i, new_id[j]), add2(new_id[j], i);
            else c[j] = i;
        }
        // 如果是割点,就和这个割点所在的 v-Dcc 连边
        // 反之染色。
    }

    // 编号 1~cnt 的是 v-Dcc, 编号 > cnt 的是原图割点
}

int main() {
    memset(head, -1, sizeof head);
    memset(Head, -1, sizeof Head);

    cin >> n >> m;
    for(int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        // 判重边
        if(u == v) continue;
        add1(u, v), add1(v, u);
    }

    for(int i = 1; i <= n; ++i) {
        if(!dfn[i]) root = i, tarjan(i);
    }
    Construct();
    return 0;
} 

其实,如果原图是无向连通图的话vdcc 缩完点之后得到的也必然是一棵树

并且,如果把割点看作白点, vdcc 看作黑点,那么这颗树必然是黑白相间的。

也可以理解为一个二分图。

证明也比较简单,证明是树的话类似 edcc。

然后问题可以转化为,树中的所有边都是一边是割点,一边是一个 vdcc。

证明其它情况不存在即可。

(其实看 Construct 里的加边也能看出来)


最后更新: May 9, 2023