割点和桥 | 无向图的双连通分量
割点和桥
桥(割边):
给定一无向连通图,对于其中一边 (u,v),若从图中删掉 (u,v)后,原图分裂成2个或以上不相连的子图(也就是图中的连通分量数增加),则称 (u,v)为原图的割边(或桥)。
割点:
给定一无向连通图,对于其中一点 u,若从图中删掉 u 和所有与 u 相连的边后,原图分裂成成 2个或以上不相连的子图(也就是图中的连通分量数增加),则称u 为原图的割点(或割顶)。
边的双连通分量 e-dcc
极大的不包含桥的连通块(每个节点之间至少用两条不含公共边的路径)
点的双连通分量 v-dcc
极大的不包含割点的连通块
tarjan 判断桥 边双连通分量
算法理解
和强连通分量一样,引入两个变量:
dfn[u] : 当前到达节点u的时间戳(dfs序)
low[u] : x能达到的时间戳最小的点
dfn很好理解,再理解下low吧。
比如说下面的图:圈内的就是dfn[u], 圈外的[1]和[6]就是low[u]。
如何求图上的桥?
通过观察可知
x和y之间是桥 <====> dnt[x]<low[y]
表示y无论如何往上走不到x
虽然找不到但是可以感性理解和证明正确性
根据定义,dnt[x]<low[y]说明从subtree(y)出发,在不经过(x,y)的前提下,无论走哪条边都无法到达x或比x更早访问的节点。
若把(x,y)删除,则subtree(y)与节点x就没有边相连,图就断开成立两部分。
因此tarjan判断桥的模板代码就是:
//防止搜反向边 引入from void tarjan(int u, int from) { //更新时间戳 dfn[u] = low[u] = ++ timestamp; //遍历图 for (int i = h[u]; i!=-1; i = ne[i]) { int j = e[i]; //如果j未遍历过,则遍历且更新low[u] if (!dfn[j]) { tarjan(j, i);//dfs(j) low[u] = min(low[u], low[j]);//用low[j]更新 //j到不了u,则x-y的边为桥, if (dfn[u] < low[j]) //正向边is_bridge[i] 反向边is_bridge[i ^ 1]都是桥 is_bridge[i] = is_bridge[i ^ 1] = true; // 这里i==idx 如果idx==奇数 则反向边=idx-1 = idx^1 // 如果idx==偶数 则反向边=idx+1 = idx^1 } // 如果j遍历过 且i不是反向边(即i不是指向u的父节点的边),则直接更新low[u] else if (i != (from ^ 1)) low[u] = min(low[u], dfn[j]);//用dfn[j]更新 } }
模板题
395. 冗余路径
题意:
新建道路 使得每两个草场之间都至少有两条分离的路径。
思路:
因为 一个边的双连通分量 <=> 任何两个点之间至少存在两个不相交路径。
所以题意就是求解加多少边才能将整个图变成双连通分量。
很明显如果本来就是双连通分量,则不需要加边,需要加边的地方就是桥。
对双连通分量做缩点 此时图上只剩桥和点 o / \ o o /\ /\ o o o o / \ o o 可以发现对左右两个叶子节点连通后,根节点连向左右叶子节点的边就可以删去了 o / \ o o /\ /\ o o-o o / \ | | o o__| | |_________| 同理 再把第2个和第4个叶子节点连通后,根节点连向第2个和第4个叶子节点的边也可以删去 第3个叶子节点随便连 给叶子节点按对称性加上边后就没有桥 <=> 变成边的双连通分量
根据上面的分析,也易得,需要加[(cnt+1)/2]条边,cnt为缩完点后度数==1的点(叶子节点)的个数
上面分析有两个问题未解决,
1. 证明:一个边的双连通分量任何两个点之间至少存在两个不相交路径
充分性: 对于每两个点都有互相分离的路径的话,则必然为强连通分量 反证 假设有桥(非双连通) x,y必然经过中间的桥 则只x→y的路径必在桥上相交 o-x 桥y-o | | ↓ | | o-o - o-o 必要性:图是一个边双连通分量 等价于 不包含桥 则一定对任意两点x,y x,y之间至少存在两条互相分离(不相交)的路径 反证:假设存在两条相交路径 那么x→y中间必然有桥 // o-o-o-o-o 蓝色路径 (从x出发到y经过边数最少的路径) // - - - - 绿色路径 // x y
2.如何缩点:
为了方便:将双连通分量缩点为该双联通分量的最高点
判断当前节点是双联通分量的最高点
对某一个点x,他走完所有的子节点之后,他的dnt[x]==low[x],那么就说明x最高遍历得到的节点就是他自己,也就是最高点了。
代码:
#include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 5010, M = 20010; int n, m; int h[N], to[M], pre[M], idx; int dfn[N], low[N], timestamp; int stk[N], top;//模拟栈,用于求出最高点 int id[N], dcc_cnt;// id记录节点i所在的双连通分量的编号;dcc_cnt为双连通分量的编号 bool is_bridge[M]; //是不是桥 int d[N]; //度数 void add(int a,int b){ to[idx]=b,pre[idx]=h[a],h[a]=idx++; } void tarjan(int u,int fa){ dfn[u]=low[u]=++timestamp; stk[++top]=u;//将遍历的元素都入栈 for(int i=h[u];i!=-1;i=pre[i]){ int j=to[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!=(fa^1)){ low[u]=min(low[u],dfn[j]); } } //如果u是最高点,则将u上面的节点出栈,并且记录他们位于哪个双连通分量 if(dfn[u]==low[u]){ ++dcc_cnt;//双连通分量数量加一 int y; do{ y=stk[top--]; 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++) //如果边i是桥 在其所连的出边的点j所在强连通分量的度+1 // 桥两边的双连通分量各+1 if (is_bridge[i]) d[id[to[i]]]++; int cnt = 0; for (int i = 1; i <= dcc_cnt; i++) if (d[i] == 1)//多少个度数为1的节点(强连通分量) cnt++;//需要加的边的数量 cout << (cnt + 1) / 2 << endl; return 0; }
tarjan 判断割点
如何求图上的割点
- 若不是搜索树的根节点(dfs的起点),
则x是割点当且仅当搜索树上存在 x 的一个子节点y,满足dfn[x] ≤ low[y]
- 若是搜索树的根节点,
则x是割点当且仅当搜索树上存在至少两个子节点 满足上述条件。
下面看图理解下:
如果x不是根节点 o | x / \ y y2 如果删除x,则y及其子节点一定不和x的父节点o相连。 如果x是根节点 如果只有一个子结点y1 x | y1 如果删除x,则y1 子节点部分还是连通的,此时x不是割点 如果有两个子结点 x / \ y1 y2 如果删除x,则y1 y2 之间不连通,此时x是割点
模板题
电力
acwing:1183
牛客
题意:
求无向图删除一个节点之后最多还剩下多少连通块
思路:
先统计连通块的个数,
- 如果不删除节点,那答案就是连通块的个数
- 如果删除节点不是割点,那答案也是连通块的个数。
因为如果不是割点,图中的连通分量数一定不增加。(割点定义) - 如果删除节点是割点,那答案就是删去割点后连通块的个数。
代码:
#include <iostream> #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; //dfn兼判重数组 int root; // 记录每个连通块的"根节点" int 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 s = 0; // 如果当前点u是割点的话,去掉该点u得到的连通分量的个数 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]) // 说明u是可能是割点, u存在一棵子树(删除割点u) s++; } else low[u] = min(low[u], dfn[j]); } //如果不是根节点 /* / u 删掉u后 除子节点yi外 / \ 还要要加上父节点部分+1 o o */ //最后还要加上父节点部分1 if (u != root) s++; // 不用加上&& s的判断,因为u不是割点的话,s要取1 ans = max(ans, s); } int main() { while (scanf("%d%d", &n, &m), n || m) { memset(dfn, 0, sizeof dfn); // dfn还具有判重数组的作用 memset(h, -1, sizeof h); idx = timestamp = 0; while (m--) { int a, b; scanf("%d%d", &a, &b); add(a, b), add(b, a); } ans = 0; //记录删除不同割点之后形成的连通块的最大值 int cnt = 0; // 记录连通块的数目 //每次将其中联通块遍历,用tarjan for (root = 0; root < n; root++) // 节点编号从0~n-1 if (!dfn[root]) { //dfn数组兼判重数组,求联通块的数量 cnt++; tarjan(root); } printf("%d\n", cnt + ans - 1); } return 0; }
tarjan 求点双连通分量
算法理解
什么是点双连通分量?
首先要明确点双连通分量和“删除割点后图中剩余的连通块”不同。
比如下图:有四个点双连通分量
很明显,对于节点两个节点对于其他连通块都是割点,但是对于因为两者都不是割点,因此是一个双连通分量。
如何求点双连通分量?
在tarjan求割点过程中,当节点满足 dfn[u] <= low[j]
时,则说明,对于栈中的u节点的子树,如果u再连接一个其他节点,u就是割点,因此此时就是一个极大的不包含割点的图。
此时栈中的u节点的子树、加上u节点就是一个点双连通分量。
如何缩点?
因为一个割点可能属于多个v-DCC,缩点方式和之前不同。
设图 中共有 p 个割点和 t 个 v-DCC,我们建立一张包含 p+t个节点的新图,把每个 vDCC和每个割点都作为新图中的节点,并在每个割点与包含它的所有v-DCC之间连边。
(容易发现,这张新图其实是一棵树(或森林))
如下图所示:
模板题
题意:
给一个不一定连通的无向图
问最少在几个点设置出口
使得任意一个出口坏掉后,其他所有点都可以和某个出口连通
分析:
首先考虑****
-
出口数量>=2
因为:如果只有一个出口 那这个出口坏了就没有可以用的出口了 -
最终方案数 = 各连通块方案数乘积
因为:不同连通块之间相互独立。
对一个连通块
-
无割点 <=> 度数==0的点<=> 孤立的点 <=> 不管我删掉哪个点 图剩余部分都是连通的
因此我们只需要设置2个出口就可以满足。 -
有割点 ,先缩点,建新图,看V-DCC度数(割点的数量)
-
- 如果V-DCC==1 意味着它只包含一个割点,必须在V-DCC内(非割点)放置一个出口
因为如果这个割点是出口且坏掉,这个V-DCC就无法连到其他出口了
- 如果V-DCC==1 意味着它只包含一个割点,必须在V-DCC内(非割点)放置一个出口
-
- 如果V-DCC>1,就不需要设置出口
如果其中一个割点坏了,则还可以到另一个割点联通的V-DCC的出口。
- 如果V-DCC>1,就不需要设置出口
代码:
#include <bits/stdc++.h> using namespace std; #define int long long typedef unsigned long long ULL; const int N = 510, M = 1010; int n, m; int to[N], pre[N], h[M], idx; int dfn[N], low[N], times; int stk[N], top; int dcc_cnt; vector<int> dcc[N];//记录双连通分量的节点 int root; bool cut[N];//判断是否为割点 void add(int a, int b) { to[idx] = b, pre[idx] = h[a], h[a] = idx++; } void tarjan(int u) { dfn[u] = low[u] = ++times; stk[++top] = u; //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 != -1; i = pre[i]) { int j = to[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); if (dfn[u] <= low[j]) { cnt++; // 判断u是否是割点 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); //弹出栈不是弹到u为止 而是弹到j为止 dcc[dcc_cnt].push_back(u); } } else low[u] = min(low[u], dfn[j]); } } int T = 1; signed main() { while (cin >> m && m) { memset(h, -1, sizeof h); for (int i = 0; i <= n; i++) dcc[i].clear(); memset(dfn, 0, sizeof dfn); memset(low, 0, sizeof low); memset(cut, 0, sizeof cut); n = idx = times = top = dcc_cnt = 0; while (m--) { int a, b; cin >> a >> b; n = max(m, max(a, 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 t : dcc[i]) { if (cut[t]) cnt++; } if (cnt == 0) { if (dcc[i].size() > 1) res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2; else res++; } if (cnt == 1) { res++, num *= dcc[i].size() - 1; } } cout << "Case " << T++ << ": " << res << " " << num << endl; } return 0; }
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/16473150.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步