割点、割边、双连通分量 一、双连通分量、割点、割边 1.割点 定义:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。人话将就是:把这个点删了,一个图会变成不连通的两个部分,我们把这个点称之为割点。

2.割边(桥) 定义:和割点有类似的定义,就是把这条边删了,整个图变成不连通的两个部分。

3.双连通 首先双连通分为:点双连通和边双连通。
对于一个图来说,点双连通就是:没有割点。边双连通就是:没有割边。
双连通的性质
点:任意两点u,v都存在2条点不相交的简单路径 (不经过重复点)。
边:任意两点u,v都存在2条不经过重复边(这两条路径可以有重复点)的简单路径 (不经过重复点)。
4.双连通分量(Biconnected Component,BCC ) 定义:找到一个极大的点集,满足导出子图是点(边)双连通的
显然这个定义并不好理解TAT,那么用人话来说是什么呢?
对于边双来说:我们把每个割边切掉,每个连通块就是边双连通分量。这个就很好理解了,我们把割边都切掉了,它还是连通的,那这个连通块就是边双的,否则如果还有割边,它一定会被切掉。
对于点双来说:比边双稍微复杂一点,因为一个点可能在多个点双里面。我们也是先把割点删了,图变成多个块,然后再把每个割点加入到不同块里面。
看下面的图会好理解一些…QAQ
点双连通分量
割点为:1,3,5,6
将割点一分为二后得到的

边双连通分量
删掉割边

5.双连通分量的缩图
边双:直接缩,缩完以后是一棵树。
点双:由于一个点可能在多个连通块里面,这个东西要怎么去表示呢?对于一个割点我们新建一个点,再向其他点双连边。
点双具体如下图:


6.总结 对于一个连通图,如果任意两点之间至少存在两条没有重复节点 的路径,则称这个图为点双连通的(简称双连通);如果任意两点之间至少存在两条没有重复边 的路径,则称该图为边双连通的。点双连通图的定义等价于任意两条边都同在一个简单环中 ,而边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,点双连通的极大子图 称为点双连通分量 (简称双连通分量),边双连通的极大子图 称为边双连通分量 。在每一个点双连通图中,内部无割点;在每一个边双连通图中,内部无桥。
二、 算法 回顾前面说的有向图里面的 ,我们把边分为4种:
Tree Edge指树边
Back Edge指连向祖先的边(返祖边)
Forward Edge指连向子孙的边(前向边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。)
Cross Edge指连向树其他分支的边(横插边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。)
在无向图只存在Tree Edge和Back Edge
那么在无向图 里面呢?
我们可以理解为:朝下的边(前向边)当成不存在的,横插边也是当成是不存在的 (因为在dfs的过程里面,我们知道横插边它已经是横插了,那连的既不是父亲也不是儿子)。以下图为例:我们在 的时候如果有这条横插边,因为是无向图我们知道 可以到 , 也可以到 ,那 到 的时候,那u就会向下 到 ,这样意味着 变成了 的儿子了,这和我们横插边是矛盾的!

那么综上所述,无向图的 树只有:返祖边和树边。 【这样之后我们tarjan里面的ins数组也就不需要啦,因为ins数组是为了区分返祖边和横插边的,但无向图的dfs树没有横插边所以不用。QAQ介个之前不明白,思考了半天wwwww】
1. 求割边 重边是会影响一条边是不是割边的,不能判掉。所以在无向图里面重边的处理要小心。
那么怎么办呢?
(代码中有讲)❗注意:这里要写它边的编号不是它父亲连下来的边的编号,而不是说判这个点是不是父亲。因为单纯判是不是父亲的话,会把重边判成同一条边,这样是错的。因为重边是会影响是不是割边
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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N = 101000 ;vector<pair<int ,int >>edge[N]; int n,m,idx;int dfn[N],low[N];vector<int >bridge; void dfs (int u,int id) { dfn[u] = low[u] = ++idx; for (auto [v,id2]:edge[u]) { if (!dfn[v]){ dfs (v,id2); low[u] = min (low[u],low[v]); if (dfn[v] == low[v])bridge.push_back (id2); } else if (id != id2){ low[u] = min (low[u],dfn[v]); } } } int main () { ios::sync_with_stdio (false ),cin.tie (0 ),cout.tie (0 ); cin>>n>>m; for (int i = 1 ;i<=m;i++) { int u,v; cin>>u>>v; edge[u].push_back ({v,i}); edge[v].push_back ({u,i}); } dfs (1 ,-1 ); sort (bridge.begin (),bridge.end ()); cout<<bridge.size ()<<"\n" ; for (auto x : bridge) cout<<x<<" " ; cout<<endl; }
2. 求割点 什么情况下确定一个点它是割点?
存在一个子树它跳不上去了。比如下图,它最多只能跳到被删去的这个点本身。

❗(代码里也有说)这里的 我们就不能乱写了,因为我们需要确定这个子树往上跳一步能跳到的地方。所以这里返祖边只能对 取 。
但是!!!有一个是反例 !
当这个点它是根且儿子个数为1,这样的话,我们把这个点删了之后整个图它还是连通的。

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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N = 101000 ;vector<int >edge[N]; int n,m,idx,sz;int dfn[N],low[N],cut[N];void dfs (int u,int fa) { dfn[u] = low[u] = ++idx; int ch = 0 ; for (auto v:edge[u]) { if (!dfn[v]){ dfs (v,u); ch++; low[u] = min (low[u],low[v]); if (low[v]>=dfn[u]) cut[u] = 1 ; } else if (v != fa) { low[u] = min (low[u],dfn[v]); } } if (u == 1 && ch <=1 )cut[u] = 0 ; sz += cut[u]; } int main () { ios::sync_with_stdio (false ),cin.tie (0 ),cout.tie (0 ); cin>>n>>m; for (int i = 1 ;i<=m;i++) { int u,v; cin>>u>>v; edge[u].push_back (v); edge[v].push_back (u); } dfs (1 ,0 ); cout<<sz<<"\n" ; for (int i = 1 ;i<=n;i++) if (cut[i]) cout<<i<<" " ; cout<<"\n" ; }
三、割点和割边的应用 题意:给一个 个点 条边的无向连通图,问最少添加多少条边使得成为一个边双 连通图。
用人话说就是:问:给你一棵边双搜完之后的树。每次可以覆盖一条路径,问最少多少次能覆盖所有?
如上图所示,黑色与绿色边为树边,红色边为非树边。每一条非树边连接的两个点都对应了树上的一条简单路径,我们说这条非树边 覆盖 了这条树上路径上所有的边。绿色的树边 至少 被一条非树边覆盖,黑色的树边不被 任何 非树边覆盖。
我们如何判断一条边是不是桥呢?显然,非树边和绿色的树边一定不是桥,黑色的树边一定是桥。
如何用算法去实现以上过程呢?首先有一个比较暴力的做法,对于每一条非树边,都逐个地将它覆盖的每一条树边置成绿色,这样的时间复杂度为O(nm) 。
结论:下界是叶 子 个 数 ,因为一条路径最多覆盖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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N = 101000 ;vector<pair<int ,int >>edge[N]; int n,m;int dfn[N],low[N],ins[N],idx,bel[N],cnt;stack<int >stk; vector<int >cc[N]; void dfs (int u,int id) { dfn[u] = low[u] = ++idx; ins[u] = true ; stk.push (u); for (auto [v,id2]:edge[u]) { if (!dfn[v]){ dfs (v,id2); low[u] = min (low[u],low[v]); } else if (id != id2){ low[u] = min (low[u],dfn[v]); } } if (dfn[u] == low[u]){ ++cnt; while (1 ) { int v = stk.top (); cc[cnt].push_back (v); ins[v] = false ; bel[v] = cnt; stk.pop (); if (u==v)break ; } } } int main () { ios::sync_with_stdio (false ),cin.tie (0 ),cout.tie (0 ); cin>>n>>m; for (int i = 1 ;i<=m;i++) { int u,v; cin>>u>>v; edge[u].push_back ({v,i}); edge[v].push_back ({u,i}); } dfs (1 ,-1 ); int nleaf = 0 ; for (int i = 1 ;i<=cnt;i++) { int cnte = 0 ; for (auto u:cc[i]) { for (auto [v,id] : edge[u]) { cnte += (bel[u] != bel[v]); } } nleaf += (cnte==1 ); } cout<<(nleaf+1 )/2 <<"\n" ; }
思路:如果不是割点,那删了这个点是不会影响整个图的连通性的,只是这个点本身不连通,那贡献是 。如果这个点是割点的话,原本的连通图会被分成若干块:这个点本身+若干被砍下来的子树+剩下的一整个部分。贡献就是那几部分大小的乘积。
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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N = 101000 ;vector<int >edge[N]; int n,m,idx;int dfn[N],low[N],sz[N];ll ans[N]; void dfs (int u,int fa) { dfn[u] = low[u] = ++idx; sz[u] = 1 ; ans[u] = n - 1 ; int cut = n - 1 ; for (auto v:edge[u]) { if (!dfn[v]){ dfs (v,u); sz[u] += sz[v]; low[u] = min (low[u],low[v]); if (low[v]>=dfn[u]){ ans[u] += (ll)sz[v]*(n-sz[v]); cut -= sz[v]; } } else if (v != fa) { low[u] = min (low[u],dfn[v]); } } ans[u] += (ll)cut*(n-cut); } int main () { ios::sync_with_stdio (false ),cin.tie (0 ),cout.tie (0 ); cin>>n>>m; for (int i = 1 ;i<=m;i++) { int u,v; cin>>u>>v; edge[u].push_back (v); edge[v].push_back (u); } dfs (1 ,0 ); for (int i = 1 ;i<=n;i++) cout<<ans[i]<<"\n" ; cout<<"\n" ; }
思路:考虑一个图它是点双(任意删一个点还是连通的)的,那至少需要2个(一个炸了还有另一个),那方案数就是 。如果这个图存在割点,那么我们去看有几个叶子,答案就是叶子个数,方案数就是除了割点以外的所有块的叶子结点的乘积。
总结:对于一般情况,割点会将整个图分为多个连通分量,我们将每个双连通分量看作整个连通块。那么一个双连通分量只需要设置一个出口。我们考虑如果只坍塌一个点,如果出口坍塌,割点就不会坍塌,那么这个分量中其他点可以通过割点到其他分量中,此时等价于同一个双连通分量。我们可以继续将这些点抽象为一个概念:叶子连通块。
什么是叶子连通块?概念引用自
叶子节点是没有出度的点,入度为1,也就是只连了一条边。那么叶子连通块的概念就是:只连接了一个割点的双连通分量。 同时,对于连接两个割点的双连通分量,其中一个割点坍塌,那么另一个割点就不会坍塌,可以通过另一个割点合并到另外一个双连通分量中。而叶子连通块就不行了,如果叶子连通块的割点(叶子的父亲)被切断,那么它就是一个单独的连通块,所以这里一定要设置一个逃生出口。在这里设置一个逃生出口,有weight种选择(weight是节点数)。根据乘法原理,最小个数是叶子连通块的个数;总方案数是所有叶子连通块的weight之积。
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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N = 101000 ;vector<int >edge[N]; int n,m,idx,cnt;int dfn[N],low[N],cut[N],sz[N];stack<int >stk; vector<int >cc[N]; void dfs (int u,int fa) { dfn[u] = low[u] = ++idx; stk.push (u); int ch = 0 ; for (auto v:edge[u]) { if (!dfn[v]){ dfs (v,u); ch++; low[u] = min (low[u],low[v]); if (low[v]>=dfn[u]){ cut[u] = 1 ; ++cnt; cc[cnt].clear (); cc[cnt].push_back (u); while (1 ) { int w = stk.top (); cc[cnt].push_back (w); sz[cnt]++; stk.pop (); if (w==v)break ; } } } else if (v != fa) { low[u] = min (low[u],dfn[v]); } } if (u == 1 && ch <=1 )cut[u] = 0 ; } int main () { ios::sync_with_stdio (false ),cin.tie (0 ),cout.tie (0 ); int t = 0 ; while (1 ) { cin>>m; if (!m)break ; for (int i = 1 ;i<=1000 ;i++) edge[i].clear (); int n = 0 ; for (int i = 1 ;i<=m;i++) { int u,v; cin>>u>>v; edge[u].push_back (v); edge[v].push_back (u); n = max ({n,u,v}); } for (int i = 1 ;i<=n;i++) dfn[i] = low[i] = cut[i] = 0 ; idx = 0 ,cnt = 0 ; while (!stk.empty ())stk.pop (); for (int i = 1 ;i<=n;i++) { if (!dfn[i]) dfs (1 ,0 ); } cout<<"Case " <<++t<<": " ; if (cnt==1 ) { int n = cc[1 ].size (); cout<<2 <<" " <<n*(n-1 )/2 <<"\n" ; } else { int ans1 = 0 ; ll ans2 = 1 ; for (int i = 1 ;i<=cnt;i++) { int ncnt = 0 ; for (auto u : cc[i])ncnt += cut[u]; if (ncnt == 1 ) { ans1 += 1 ; ans2 *= (int )cc[i].size ()-1 ; } } cout<<ans1<<" " <<ans2<<"\n" ; } } }
题意:点 条边的无向图,保证图连通。找到两个点 ,使得 到必须 经过的边最多(一条边无论走哪条路线都经过ta,这条边就是必须经过的边),$2<=n<=310^5,1<=m<=3 10^5$
思路:因为是必须要走的边,思考什么是必须要走的边?如果有环存在,那可以走的边肯定不是唯一的。在一个边双连通分量里面,任意两点之间至少存在两条无重边的简单路径。那么一个同一个边双内的点是没有必须经过的边的。那么我们可以考虑边双缩点。缩完点以后是一棵树,那么此时任意两点的路径都是唯一的了,我们要求最长的话,那就是树的直径。
四、总结 1.遇到非连通图几乎可以肯定是要求连通分量,不论是无向还是有向图。(可以节约大量思考时间)
2.凡是对边、点的操作,在同一个分量内任意一个点效果相同的,几乎都是缩点解决问题;再粗暴点,几乎求了连通分量都要缩点;
3.一定要考虑特殊情况,如整个图是一个连通分量等。
4.对于双连通分量要分析是边还是点双连通分量。
5.拿到题目要先搞清楚给的是连通图还是非连通图。
对于什么时候用点双,什么时候用边双这个问题,个人感觉是,点双的性质其实比边双要好,因为边双里面允许有割点的存在,边双只强调点集内所有点能相互到达,但点双就有一些更好的性质,比如像吃掉一个点之后剩下的点之间还能相互到达。