无向图的双连通分量(DCC) | 字数总计: 2.9k | 阅读时长: 11分钟 | 阅读量:
无向图的双连通分量(DCC)
可分为 边双连通分量(e-DCC) 与 点双连通分量(v-DCC)
注意:
点双连通分量不一定是边双连通分量,边双连通分量也不一定是点双连通分量
割点与桥没有 什么本质的关系
两个割点之间的边不一定是桥
一个桥的两个端点不一定是割点
边双连通分量(e-DCC) 介绍
桥:一条无向边,若删除此边,整个图变得不连通,则称这条边为“桥”。
边双连通分量:极大的不包含“桥”的连通块,则称为“边的双连通分量”
性质:
无论删除哪一条边,整个图一定还是连通的
任意两点之间一定包含两条不相交的路径(边不相交)。
Tarjan算法求边的双连通分量
时间戳(timestamp),dfn[N], low[N], 和有向图的强连通分量作用相同
如何找到桥?
若子节点可以走到父节点或祖先节点,则从父节点到子节点的边不是桥,等价于,若边 x-y 为桥,则 dfn[x] < low[y]
如何找到边双连通分量?
方法一:将所有桥删去
方法二:类似于有向图的强连通分量的Tarjan算法,用栈(stk数组)维护,刚开始搜索加到栈里,若dfs[i] == low[i]的话(即、i的父节点到i这条边为桥),则i当前所在的子树中的节点即为当前边的双连通分量中的点。
例题: 395. 冗余路径
题意:
给定一个无向连通图, 问:最少加几条边,使得任何两点之间都至少有两条边;等价于,最少加几条边,可以将其变成边双连通分量。
结论1:
任何两点之间都至少有两条边;等价于,此图为边双连通分量
证明1:
充分性:左 -> 右
如果 任何两点之间都至少有两条边 则 此图是边双连通分量
反证法:假设任何两点之间都包含两条互不相交的路径,如果其不是边的双连通分量,则意味着这个图中,至少存在一个桥。
在桥两侧的连通块中任意找两个点x, y,则x到y的所有路径都经过桥这条边,所以x, y两点之间不存在两条互不相交的路径,与条件中的 任何两点直接按都包含两条互不相交的路径相矛盾,则假设不成立,原命题成立。
必要性:左 <- 右
如果 此图是边双连通分量 则 任何两点之间都至少有两条边
左可推:此图不含桥,假设两点 x, y 则 x, y 的最短路径为连接 x, y 的一条边,对于该边中的每一段边,都可以直到这个边不是桥,则两个连通块之间必定有两外一条边,,基本思路为如此,具体证明y也不太清楚hh。
结论2:
如果度数为 1 的点的个数为 cnt 个,则需要新加 (cnt + 1) / 2 条边,可以使得此图变为边的双连通分量。
证明2:
证明 ans >= (cnt + 1) / 2 : 若此图边为边的双连通分量,等价于所有点的度数 >= 2 , 即、所有度数为 1 的点都至少需要加一条边,最优先的连边策略为两个度数为 1 的点连一条边,消除两个度数为 1 的点,如果剩余一个点,此点随便连一个点即可,如此新加的边的个数的最优策略为 cnt / 2上取整,等价于 (cnt + 1) / 2 下取整,即所求新加的边 ans >= (cnt + 1) / 2 (最优解)。
证明 ans == (cnt + 1) / 2 : 可知如果可以取到 (cnt + 1) / 2,则 我们所求的 ans == (cnt + 1) / 2;
边界:当 只有两个点时,加一条边即可,(2 + 1) / 2 == 1 成立
至少有三个点:以一个度数至少为 2 的点为根节点,则所有度数为 1 的点都为叶子节点,两个叶子节点相连,剩余一个随便连,则新加的边的数量为 (cnt + 1) / 2,也成立。
大致证明为以上的叙述,,具体证明太麻烦,就此告一段落,记住结论即可。
代码如下: 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 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 5010 , M = 20010 ;int n, m;int h[N], e[M], ne[M], idx;int dfn[N], low[N], timestamp;int stk[N], top;int dcc_id[N], dcc_cnt;bool is_bridge[M];int d[N];void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan (int u, int from) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan (j, i); low[u] = min (low[u], low[j]); if (dfn[u] < low[j]) is_bridge[i] = is_bridge[i ^ 1 ] = true ; } else if (i != (from ^ 1 )) low[u] = min (low[u], dfn[j]); } if (dfn[u] == low[u]) { ++ dcc_cnt; int y; do { y = stk[top -- ]; dcc_id[y] = dcc_cnt; } while (y != u); } } int main () { cin >> n >> m; memset (h, -1 , sizeof h); while (m -- ) { int a, b; cin >> a >> b; add (a, b), add (b, a); } tarjan (1 , -1 ); for (int i = 0 ; i < idx; i ++ ) if (is_bridge[i]) d[dcc_id[e[i]]] ++; int cnt = 0 ; for (int i = 1 ; i <= dcc_cnt; i ++ ) if (d[i] == 1 ) cnt ++; printf ("%d\n" , (cnt + 1 ) / 2 ); return 0 ; }
点双连通分量(v-DCC) 介绍
割点:一个点,若删除此点,整个图变得不连通,则称这个点为“割点”
点双连通分量:极大的不包含“割点”的连通块,则称为“点的双连通分量”。
性质:
每一个割点都会至少属于两个点双连通分量。
Tarjan算法求点的双连通分量
时间戳(timestamp),dfn[N], low[N], 和有向图的强连通分量作用相同
如何找到割点?若,对于 x - y,在 low[y] >= dfn[x] (最多只能走到父节点) 的情况下:
如果 x 不是根节点,那么 x 必定是一个割点
如果 x 是根节点,至少有两个子节点,那么 x 必定是一个割点
如何求点的双连通分量?
stack实现1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (u == 孤立点){ cnt ++ ; 此点单独为一个点的双连通分量; } if (dfn[x] <= low[y]){ cnt ++ ; if (x != 根节点 || cnt > 1 ) x 是割点; 将栈中元素弹出,直至弹出 y 为止。 且 x 也属于此点的双连通分量。 }
证明为什么将 x 加入 y 的双连通分量中:
若 dfn[x] <= low[y] 可以取到 = ,则 x 在此双连通分量是无可厚非的(必然的,正确的)
若 dfn[x] < low[y] ,则 缩点后 x - y 只有一条边相连,两点一边本身就是一个点双连通分量,所以将 x 加入此双连通分量也是正确的。
例题: 1183. 电力
题意: 删除一个点,可以形成的最多的连通块的个数为多少?
思路: 求割点
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 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 10010 , M = 30010 ;int n, m;int h[N], e[M], ne[M], idx;int dfn[N], low[N], timestamp;int root, ans;void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan (int u) { dfn[u] = low[u] = ++ timestamp; int cnt = 0 ; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan (j); low[u] = min (low[u], low[j]); if (low[u] >= dfn[u]) cnt ++; } else low[u] = min (low[u], dfn[j]); } if (u != root) cnt ++; ans = max (ans, cnt); } int main () { while (scanf ("%d%d" , &n, &m), n || m) { memset (h, -1 , sizeof h); memset (dfn, 0 , sizeof dfn); idx = timestamp = 0 ; while (m -- ) { int a, b; scanf ("%d%d" , &a, &b); add (a, b), add (b, a); } ans = 0 ; int cnt = 0 ; for (root = 0 ; root < n; root ++ ) if (!dfn[root]) { cnt ++; tarjan (root); } printf ("%d\n" , ans + cnt - 1 ); } return 0 ; }
例题: 396. 矿场搭建
题意; 给定一个无向图,问最少在几个点上设置出口,可以使得不管其他哪个点坍塌,其余所有点都可以与某个出口相连;
结论1: :
出口数量 >= 2
分别看每个连通块:
(v-DCC 度数为0) 若 图中无割点(无论删除哪个点,剩余部分一定为连通的):若 cnt 为图中点的个数, ans == C(2, cnt) == cnt * (cnt - 1) / 2
若 图中有割点:则需要缩点!
每个割点单独作为一个点
从每个 v-DCC 向其所包含的每个割点连一条边
v-DCC 度数为 1 :需要在该分量内部(非割点)放一个出口
v-DCC 度数 > 1 :无需设置出口
证明1:
缩点过,如果割点 坍塌,已知度数为 1 的点是叶子节点,则每一颗子树至少能被自己的叶子节点所救
如果度数为 1 的 v-DCC 中的某个点坍塌,可以通过割点走向另一个子树的叶子节点,从而逃脱
如果度数为 2 的 v-DCC 中的某个点坍塌,可以通过走到任意的一个割点从而走到子树的叶子节点,从而逃脱
代码如下:
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 #include <iostream> #include <algorithm> #include <cstring> #include <vector> using namespace std;typedef unsigned long long ULL;const int N = 1010 , M = 1010 ;int n, m;int h[N], e[M], ne[M], idx;int dfn[N], low[N], timestamp;int stk[N], top;int dcc_cnt;vector<int > dcc[N]; bool cut[N];int root;void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan (int u) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u; if (u == root && h[u] == -1 ) { dcc_cnt ++; dcc[dcc_cnt].push_back (u); return ; } int cnt = 0 ; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan (j); low[u] = min (low[u], low[j]); if (dfn[u] <= low[j]) { cnt ++; if (u != root || cnt > 1 ) cut[u] = true ; ++ dcc_cnt; int y; do { y = stk[top -- ]; dcc[dcc_cnt].push_back (y); } while (y != j); dcc[dcc_cnt].push_back (u); } } else low[u] = min (low[u], dfn[j]); } } int main () { int T = 0 ; while (cin >> m, m) { for (int i = 1 ; i <= dcc_cnt; i ++ ) dcc[i].clear (); idx = n = timestamp = top = dcc_cnt = 0 ; memset (h, -1 , sizeof h); memset (dfn, 0 , sizeof dfn); memset (cut, 0 , sizeof cut); while (m -- ) { int a, b; cin >> a >> b; n = max (n, a), n = max (n, b); add (a, b), add (b, a); } for (root = 1 ; root <= n; root ++ ) if (!dfn[root]) tarjan (root); int res = 0 ; ULL num = 1 ; for (int i = 1 ; i <= dcc_cnt; i ++ ) { int cnt = 0 ; for (int j = 0 ; j < dcc[i].size (); j ++ ) if (cut[dcc[i][j]]) cnt ++; if (cnt == 0 ) { if (dcc[i].size () > 1 ) res += 2 , num *= dcc[i].size () * (dcc[i].size () - 1 ) / 2 ; else res ++; } else if (cnt == 1 ) res ++, num *= dcc[i].size () - 1 ; } printf ("Case %d: %d %llu\n" , ++ T, res, num); } return 0 ; }