双连通分量的题目列表(一)
双连通分量:定义:给一个无向图,其中的极大子图中的每个点两两可达,那么就说明这个是一个双连通分量。
点双连通:如果任意两点至少存在两条点不重复的路径,则说明这个图是点双连通。
点双连通的一些特点
①每条边恰好属于一个双连通分量,但不同的双连通分量可能有公共点。
②不同的双连通分量最多只能有一个公共点
③任意割顶都是至少两个不同的双连通分量之间路径的公共点(去除顶点的特殊情况)
首先这个人的博客挺好的:https://www.byvoid.com/blog/biconnect/
然后下面是我自己总结的一个东西
边双连通:如果任意的两点至少存在两条便不重复的路径,那么就说明是边双连通。
①除了桥不属于任何边双连通分量之外,其他每条边恰好属于一个变双连通分量
②把桥删除之后,每个连通分量对应原图的一个边双连通分量
双连通分量取出来的是环(除了只有两个顶点的,而且这两个顶点不算双连通,例如0-0)
题目列表
①点双连通分量+二分图染色判奇偶环 LA 3523 圆桌骑士(一)
②利用点双连通最小方案数问题 LA 5135 井下矿工(二)
③边双连通分量+缩点:加入最少边数能让图变成双连通图 POJ 3352(三) 经典题目 和LA 4287 强连通最后的ans的判断方法做一下区别(强连通一)
④
⑤
⑥
一:圆桌骑士 UVALIVE3523 蓝书316
题目大意:n个歧视,有m个憎恶关系,现在让n个骑士开会,相互憎恶的骑士不能出现在同一个会上,且开会的人数为奇数。问有多少个骑士在任何一个会上都不能出现?
思路:首先我们反转关系,将没有憎恶关系的骑士之间连边。然后我们得到另外一个题目,即有多少个骑士能出现在奇环中。然后再分析一下二分图,二分图形成环一定是偶数环,所以我们现在的目的是取出所有的环。然后进行二分图染色,如果不能成功染色的,说明是可以成功匹配的,就用保存下来就好了。
//看看会不会爆int! 或者绝对值问题。 #include <bits/stdc++.h> using namespace std; #define ll long long #define pb push_back #define mk make_pair #define fi first #define se second #define all(a) a.begin(), a.end() const int maxn = 1000 + 5; const int maxm = 1000000 + 5; int n, m; vector<int> G[maxn], bcc[maxn]; int bccno[maxn]; int a[maxn][maxn]; int dfstime, bcnt; int lowu[maxn], pre[maxn]; bool iscut[maxn]; stack<pair<int, int> > s; int find_bcc(int u, int fa){ int child = 0; int lowu = pre[u] = ++dfstime; int len = G[u].size(); for (int i = 0; i < len; i++){ int v = G[u][i]; pair<int, int> p = mk(u, v); if (pre[v] == -1){ s.push(p); child++; int lowv = find_bcc(v, u); lowu = min(lowv, lowu); if (lowv >= pre[u]){ iscut[u] = true; bcnt++; while (true){ pair<int, int> g = s.top(); s.pop(); if (bccno[g.fi] != bcnt) bcc[bcnt].pb(g.fi), bccno[g.fi] = bcnt; if (bccno[g.se] != bcnt) bcc[bcnt].pb(g.se), bccno[g.se] = bcnt; if (g.fi == u && g.se == v) break; } } } else if (pre[v] < pre[u] && v != fa){ s.push(p); lowu = min(lowu, pre[v]); } } if (fa < 0 && child == 1) iscut[u] = false; return lowu; } void init(){ dfstime = bcnt = 0; memset(a, 0, sizeof(a)); memset(iscut, false, sizeof(iscut)); memset(pre, -1, sizeof(pre)); for (int i = 1; i <= n; i++){ G[i].clear(); bcc[i].clear(); } for (int i = 1; i <= m; i++){ int u, v; scanf("%d%d", &u, &v); a[u][v] = a[v][u] = 1; } for (int i = 1; i <= n; i++){ for (int j = i + 1; j <= n; j++){ if (a[i][j] == 0){ G[i].pb(j); G[j].pb(i); } } } } int color[maxn]; bool draw(int u, int ty){ int len = G[u].size(); for (int i = 0; i < len; i++){ int v = G[u][i]; if (bccno[v] != ty) continue; if (color[v] == color[u]) return false; if (color[v] == -1){ color[v] = 1 - color[u]; if (!draw(v, ty)) return false; } } return true; } int odd[maxn]; int main(){ while (~scanf("%d%d", &n, &m) && (n + m) > 0){ init(); memset(odd, 0, sizeof(odd)); for (int i = 1; i <= n; i++){ if (pre[i] == -1){ find_bcc(i, -1); } } //目前找奇数环 for (int i = 1; i <= bcnt; i++){ int len = bcc[i].size(); memset(color, -1, sizeof(color)); for (int j = 0; j < len; j++){ int v = bcc[i][j]; bccno[v] = i; } color[bcc[i][0]] = 0; if (!draw(bcc[i][0], i)){ for (int j = 0; j < len; j++){ int v = bcc[i][j]; odd[v] = true; } } for (int j = 0; j < len; j++){ int v = bcc[i][j]; //printf("%d%c", color[v], j == len - 1 ? '\n' : ' '); } } int ans = n; for (int i = 1; i <= n; i++){ if (odd[i]) ans--; } printf("%d\n", ans); } return 0; }
关键:关系图的反转、二分图的使用
二:井下矿工 LA 5135 蓝书318
题目大意:有n条路矿井,每条路连接两个顶点,没有重边。你的任务是在节点处安装太平井,不得不管哪个连接点倒塌,不在此连接点的所有旷工都能到达太平井(只有倒塌的那条路不能走)。问,选择安装太平井的最小数目,和该数目下的最小方案数。
思路一:点双连通分量
假设安装太平井的地方涂黑。我们发现,涂黑点的最优点一定不会是割点,因为如果涂黑这里,那么如果这里坏了,上下两条路就不相通,所以还要再额外在上下两条路在添加一个黑点。因此我们发现,在双连通分量里面只要涂黑不是割点的地方就行了。而且我们还发现,一个双连通分量里面如果有两个割点,那么这个双连通分量就不需要添加黑点了。
//看看会不会爆int! 或者绝对值问题。 #include <bits/stdc++.h> using namespace std; #define LL long long #define pb push_back #define mk make_pair #define fi first #define se second #define all(a) a.begin(), a.end() const int maxn = 100000 + 5; vector<int> G[maxn], bcc[maxn]; int n, m, dfstime, bcccnt; bool iscut[maxn]; int bccnu[maxn], pre[maxn]; stack<pair<int, int> > s; int dfs(int u, int fa){ int lowu = pre[u] = ++dfstime; int child = 0; int len = G[u].size(); for (int i = 0; i < len; i++){ int v = G[u][i]; pair<int, int> p = mk(u, v); if (pre[v] == -1){ child++; s.push(p); int lowv = dfs(v, u); lowu = min(lowu, lowv); if (lowv >= pre[u]){ iscut[u] = true; bcccnt++; while (true){ pair<int, int> pr = s.top(); s.pop(); if (bcccnt != bccnu[pr.fi]) bccnu[pr.fi] = bcccnt, bcc[bcccnt].pb(pr.fi); if (bcccnt != bccnu[pr.se]) bccnu[pr.se] = bcccnt, bcc[bcccnt].pb(pr.se); if (pr.fi == u && pr .se == v) break; } } } else if (pre[v] < pre[u] && v != fa){ s.push(p); lowu = min(lowu, pre[v]); } } if (fa < 0 && child == 1) iscut[u] = false; return lowu; } void find_bcc(){ dfstime = bcccnt = 0; memset(iscut, false, sizeof(iscut)); memset(bccnu, 0, sizeof(bccnu)); memset(pre, -1, sizeof(pre)); dfs(1, -1); } map<int, int> mp; int main(){ int kase = 0; while (scanf("%d", &m) == 1 && m){ mp.clear(); for (int i = 1; i <= m * 2; i++) { G[i].clear(); bcc[i].clear(); } n = 0; for (int i = 1; i <= m; i++){ int u, v; scanf("%d%d", &u, &v); if (mp[u] == 0) mp[u] = ++n; if (mp[v] == 0) mp[v] = ++n; u = mp[u], v = mp[v]; //printf("u = %d v = %d\n", u, v); G[u].pb(v); G[v].pb(u); } find_bcc(); LL num = 0, ans = 1; if (bcccnt == 1){ num = 2; ans = 1LL * bcc[1].size() * (bcc[1].size() - 1) / 2; } else { for (int i = 1; i <= bcccnt; i++){ int len = bcc[i].size(); int cnt = 0; for (int j = 0; j < len; j++){ if (iscut[bcc[i][j]]) cnt++; } if (cnt == 1){ num++; ans = ans * 1LL * (len - cnt); } } } //printf("bcccnt = %d\n", bcccnt); printf("Case %d: %lld %lld\n", ++kase, num, ans); } return 0; }
关键:点双对割点的运用
思路二:割点
通过dfs求出割点的位置,然后再对不是割点的进行dfs,如果经过两个不同的割点,那么就不乘,反之乘以dfs下来的数目。
三:边双连通题+缩点 POJ 3352
题目大意:给一张图,最少加入多少个图片能使得原来的图变成双连通图?
思路:dfs求出所有的桥,然后利用边双连通分量来得到每个极大子图。每个极大子图都对应着一个bcc_cnt,然后极大子图里面是保证了是双连通的。那么题目就换成了,两个bcc_cnt不同的子图之间有几条边,如果边数是1,就说明要加边。(这里利用的思想就是把bcc_cnt的这个极大子图看成一个点来对待,看看每个点之间有几条边)
1 //看看会不会爆int! 或者绝对值问题。 2 #include <cstdio> 3 #include <cstring> 4 #include <iostream> 5 #include <algorithm> 6 #include<stack> 7 #include<vector> 8 using namespace std; 9 #define LL long long 10 #define pb push_back 11 #define mk make_pair 12 #define fi first 13 #define se second 14 #define all(a) a.begin(), a.end() 15 const int maxn = 1000 + 5; 16 stack<int> s; 17 vector<int> G[maxn], bcc[maxn]; 18 int bccno[maxn], pre[maxn], iscut[maxn]; 19 int n, m, bcc_cnt, dfstime; 20 21 int dfs(int u, int fa){ 22 int lowu = pre[u] = ++dfstime; 23 int len = G[u].size(); 24 int child = 0; 25 s.push(u); 26 for (int i = 0; i < len; i++){ 27 int v = G[u][i]; 28 if (pre[v] == -1){ 29 child++; 30 int lowv = dfs(v, u); 31 lowu = min(lowv, lowu); 32 if (lowv > pre[u]){ 33 iscut[u] = true; 34 } 35 } 36 else if (pre[v] < pre[u] && v != fa){ 37 lowu = min(lowu, pre[v]); 38 } 39 } 40 if (lowu == pre[u]){ 41 bcc_cnt++; 42 while (true){///边双连通分量是不存在重点的 43 int v = s.top(); s.pop(); 44 bcc[bcc_cnt].pb(v); 45 if (v == u) break; 46 } 47 } 48 if (fa == -1 && child == 1) iscut[u] = false; 49 return lowu; 50 } 51 int cnt[maxn], color[maxn]; 52 int main(){ 53 while (scanf("%d%d", &n, &m) == 2){ 54 for (int i = 1; i <= n; i++){ 55 G[i].clear(); bcc[i].clear(); 56 } 57 for (int i = 1; i <= m; i++){ 58 int u, v; scanf("%d%d", &u, &v); 59 G[u].pb(v), G[v].pb(u); 60 } 61 memset(bccno, 0, sizeof(bccno)); 62 memset(iscut, false, sizeof(iscut)); 63 memset(pre, -1, sizeof(pre)); 64 dfstime = bcc_cnt = 0; 65 for (int i = 1; i <= n; i++){ 66 if (pre[i] == -1){ 67 dfs(i, -1); 68 } 69 } 70 memset(cnt, 0, sizeof(cnt)); 71 memset(color, 0, sizeof(color)); 72 int ans = 0;///我们需要知道在连通分量里面有几条边 73 74 for (int i = 1; i <= bcc_cnt; i++){ 75 int len = bcc[i].size(); 76 for (int j = 0; j < len; j++){ 77 int v = bcc[i][j]; 78 color[v] = i; 79 } 80 } 81 for (int i = 1; i <= n; i++){ 82 int len = G[i].size(); 83 for (int j = 0; j < len; j++){ 84 int v = G[i][j]; 85 if (color[i] != color[v]) { 86 cnt[color[i]]++; 87 } 88 } 89 } 90 for (int i = 1; i <= bcc_cnt; i++){ 91 if (cnt[i] == 1) ans++; 92 } 93 printf("%d\n", (ans + 1) / 2); 94 } 95 return 0; 96 }
学习:边双连通的做法,缩点的技巧,任意两个边双连通是不存在公共点的
四:
五:
六:
七: