割点与割边
连通分量
有关无向图连通性问题最基本的概念,可以引申出双连通分量(三连通分量)等问题。
定义:一个图内的极大连通子图(即任意两点可以互相到达)称为这个图的一个点双连通分量。
一个图被它的各个连通分量分为数个连通图。
割点
定义:如果在一个图中删去一个点与跟其连接的所有边,整个图的连通分量变多的话,则称这个点是一个割点。
如何判断割点
1.DFS树
这是一个一般图,以节点一为根节点用 DFS 的顺序将其画成树的样子。
去除那些跨度超过一层的边(如边 < 1 , 6 > , < 3 , 6 > <1,6>,<3,6> <1,6>,<3,6>,这些边在后文被称为返祖边)可以得到一棵树,即原图以节点一为根节点的 DFS 树。
然后可以得到其中一种 DFS 序: { 1 , 3 , 2 , 6 , 4 , 5 } \{1,3,2,6,4,5\} {1,3,2,6,4,5}
其中每个节点都有自己的 d f n : { 1 , 3 , 2 , 5 , 6 , 4 } dfn:\{1,3,2,5,6,4\} dfn:{1,3,2,5,6,4}。
d f n dfn dfn 即一个节点在 DFS 序中的位置。
容易发现,DFS 树也是原图的生成树之一。
2.判断割点
割点判定法则:在 DFS 过程中,如果一个点的子节点没有任何一个点可以通过返祖边绕回其祖先,则这个点是一个割点。
额外增加一个 l o w low low 数组,代表这个节点通过一条返祖边能够绕回的 d f n dfn dfn 最小的节点的 d f n dfn dfn。
返祖边即 DFS 树树边以外的边。
特别的,对于根节点,如果其在 DFS 树中有两个及以上子节点,则其是割点。
void dfs(int now,int fa=-1){
int son=0;
dfn[now]=low[now]=++cnt;
for(auto it:e[now]){
if(!dfn[it]){
son++;
dfs(it,now);
low[now]=min(low[now],low[it]);
if(low[it]>=dfn[now]&&fa!=-1) is_cut[now]=1;
}else if(it!=fa) low[now]=min(low[now],dfn[it]);
}
if(fa==-1&&son>1) is_cut[now]=1;
}
example
【模板】割点(割顶)
模板题,正常求割点即可。
#include<bits/stdc++.h>
using namespace std;
int n,m,dfn[20005],low[20005],cnt,ans;
bool is_cut[20005];
vector<int>e[20005];
void dfs(int now,int fa=-1){
int son=0;
dfn[now]=low[now]=++cnt;
for(auto it:e[now]){
if(!dfn[it]){
son++;
dfs(it,now);
low[now]=min(low[now],low[it]);
if(low[it]>=dfn[now]&&fa!=-1) ans+=!is_cut[now],is_cut[now]=1;
}else if(it!=fa) low[now]=min(low[now],dfn[it]);
}
if(fa==-1&&son>1) ans+=!is_cut[now],is_cut[now]=1;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) dfs(i);
}
cout<<ans<<"\n";
for(int i=1;i<=n;i++){
if(is_cut[i]) cout<<i<<" ";
}
}
割边
定义:如果在一个图中删去一条边,整个图的连通分量变多的话,则称这条边是一个割边。
割边判定法则:在 DFS 过程中,如果一个点的子节点没有任何一个点可以通过返祖边绕回其或其祖先,则这个点通向其儿子的边一个割边。
跟割点差不多,不过有一点小区别。首先,割边增加了不能绕回其的条件。
所以割边的判断条件为 if(low[it]>dfn[now])
。同时因为割边只存在于 DFS 树上,所以可以把标记打在边上或儿子上。
对于根节点的的特判也可以省略。
void dfs(int now,int fa=-1){
int son=0;
dfn[now]=low[now]=++cnt;
for(auto it:e[now]){
if(!dfn[it]){
son++;
dfs(it,now);
low[now]=min(low[now],low[it]);
if(low[it]>dfn[now]) is_cut[now]=1;
}else if(it!=fa) low[now]=min(low[now],dfn[it]);
}
}
边双与点双
点双连通分量
定义:点双连通分量即一个不存在割点的极大连通子图。
当我们遍历的时候,可以用一个栈记录遍历过的点。
当我们访问完一个割点的时候,其实就已经完成了一个极大点双连通分量的访问。
如果满足节点 u u u 为割点,那么 u u u 和其刚刚遍历的子节点 v v v 共同作为一个点双连通分量的一部分。
此时将栈中的点弹出来,加入一个新的点双连通分量,直到加入 v v v 为止,最后再将 u u u 加进去。
为什么不弹出 u u u
当遍历完以 v v v 为根的子树后,栈的结构可能是这样: s t = { u , ⋯ , v , ⋯ } \mathrm{st}=\{u,\cdots,v,\cdots\} st={u,⋯,v,⋯}
其中 u u u 到 v v v 之间的点是因为能够返回 u u u 的祖先所以在栈中保留下来。所以不能被弹出。同时 u u u 作为一个割点,可能同时属于多个点双连通分量,所以不能弹出。
example
求点双连通分量,按思路打就可以。
#include<bits/stdc++.h>
using namespace std;
long long n,m;
int dfn[500005],low[500005],st[500005],point_num,cnt,tp;
vector<int>e[500005],ans[500005];
void dfs(int now,int fa){
int son=0;
dfn[now]=low[now]=++cnt;
st[++tp]=now;
for(auto it:e[now]){
if(!dfn[it]){
son++;
dfs(it,now);
low[now]=min(low[now],low[it]);
if(dfn[now]<=low[it]){
point_num++;
while(st[tp+1]!=it) ans[point_num].push_back(st[tp--]);
ans[point_num].push_back(now);
}
}else if(it!=fa) low[now]=min(low[now],dfn[it]);
}
if(fa==-1&&son==0) ans[++point_num].push_back(now);//特判孤立点
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tp=0;
dfs(i,-1);
}
}
cout<<point_num<<"\n";
for(int i=1;i<=point_num;i++){
cout<<ans[i].size()<<" ";
for(auto it:ans[i]) cout<<it<<" ";
cout<<"\n";
}
}
边双连通分量
定义:边双连通分量即一个不存在割点的极大连通子图。
既然边双连通分量里不存在割边,那么我们把所有割边去掉(即打上标记),这样剩下的每一个连通块都是一个边双连通分量。
代码即为求割边的代码加上一个 dfs。这里就不放了。
example
注意到数据可能有重边,所以应该给每一条边标上编号或者链式前向星奇偶边。
#include<bits/stdc++.h>
using namespace std;
struct edge{int to,num;};
int n,m,dfn[500005],low[500005],ine_dcc[500005],cnt,num;
bool e_cut[2000005];
vector<edge>e[500005];
vector<int>e_dcc[500005];
void dfs(int now,int last_num=-1){
dfn[now]=low[now]=++cnt;
for(auto it:e[now]){
if(!dfn[it.to]){
dfs(it.to,it.num);
low[now]=min(low[now],low[it.to]);
if(low[it.to]>dfn[now]) e_cut[it.num]=1;
}else if(it.num!=last_num) low[now]=min(low[now],dfn[it.to]);
}
}
void get(int now,int num){
ine_dcc[now]=num;
e_dcc[num].push_back(now);
for(auto it:e[now]){
if(!ine_dcc[it.to]&&!e_cut[it.num]) get(it.to,num);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
e[u].push_back({v,i});
e[v].push_back({u,i});
}
for(int i=1;i<=n;i++){
if(!dfn[i]) dfs(i);
}
for(int i=1;i<=n;i++){
if(!ine_dcc[i]){
num++;
get(i,num);
}
}
cout<<num<<"\n";
for(int i=1;i<=num;i++){
cout<<e_dcc[i].size()<<" ";
for(auto it:e_dcc[i]) cout<<it<<" ";
cout<<"\n";
}
}
缩点
点双的缩点较为复杂,这里不做叙述(link)。
边双的缩点较为好理解,将每一个边双连通分量缩点,缩点后的图,点与点之间就以割边连接(最大的一个性质就是有环图变成了无环图),这样的话可以在一些特殊情况下,可以套树链剖分,树形 DP 之类的算法。
end.