这一期我们来讲解图论里面的割点、割边、边双连通分量和点双联通分量。
要解决这些问题,我们要再次请出我们的Tarjan大佬。没错,这几个问题的算法也是他发明的。
所以,我们要用dfs来解决这个问题。
我们先来解决割点。
割点:
割点的概念:在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图中连通分量数量变多,那么这个点就叫做割点。
如何求呢?
最容易想到的就是一次删除每个节点,然后dfs判断是否连通。
这个方法效率太低了,而且太low了,我们这里用一下更高级的方法。
这里我们需要用到dfs序,我们定义dfn[i]为dfs到i点时的序号。这里我们还需要定义一个东西——low[i],它代表什么呢?它表示low[u]表示顶点u及其子树中的点,通过非父子边,能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。
如何求解呢?我们可以在dfs中求解。
假设当前顶点为u,则默认low[u]=dfn[u],即最早只能回溯到自身。
有一条边(u, v),如果v未访问过,继续DFS,DFS完之后,low[u]=min(low[u], low[v]);
如果v访问过且u不是v的父亲,就不需要继续DFS了,一定有dfn[v]<dfn[u],low[u]=min(low[u], dfn[v])。
对于节点u,如果它有一个子节点v,使得low[v]>=dfn[u],那么此时u就是割点。
对于根节点,如果此时它有两个子节点,都使得low[root]>=dfn[u],那么此时根节点就是割点
这样子我们就可轻松求解了。
CODE:
#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> a[100005];
int dfn[100005],low[100005];
int t=1;
int ans=0;
bool flag[100005];
void dfs(int x,int fa){
dfn[x]=t++;
low[x]=dfn[x];
int child=0;
for(int ti=0;ti<a[x].size();ti++){
int i=a[x][ti];
if(dfn[i]==0){
child++;
dfs(i,x);
if(dfn[x]<=low[i] && x!=1)
flag[x]=1;
else if(x==1 && child==2)
flag[x]=1;
if(low[i]<low[x])
low[x]=low[i];
}
else if(i!=fa&&dfn[i]<low[x])
low[x]=dfn[i];
}
}
int main(){
cin>>n>>m;
int tx,ty;
for(int i=1;i<=m;i++){
cin>>tx>>ty;
a[tx].push_back(ty);
a[ty].push_back(tx);
}
dfs(1,0);
for(int i=1;i<=n;i++)
if(flag[i])
ans++;
cout<<ans;
}
模板题:P3388 【模板】割点(割顶)
割边:
割边的概念:在无向连通图中,如果将其中一条边去掉,图中连通分量数量变多,那么这个点就叫做割点。
跟割点类似,我们定义的low和dfn数组的意义不变,但是判断(u,v)的标准变成当且仅当(u,v)为搜索树中的边并且dfn(u)<low(v) 。
所以我们也可以轻松解决。
CODE:
#include<bits/stdc++.h>
using namespace std;
vector<int> a[100005];
int dfn[100005],low[100005],t=1,ans=0,n,m;
bool flag[100005];
void dfs(int x,int fa){
dfn[x]=t++;
low[x]=dfn[x];
int child=0;
for(int ti=0;ti<a[x].size();ti++){
int i=a[x][ti];
if(dfn[i]==0){
child++;
dfs(i,x);
if(dfn[x]<low[i])ans++;
if(low[i]<low[x])low[x]=low[i];
}
else if(i!=fa&&dfn[i]<low[x])low[x]=dfn[i];
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int tx,ty;
cin>>tx>>ty;
a[tx].push_back(ty);
a[ty].push_back(tx);
}
dfs(1,0);
cout<<ans;
}
模板题:【模板】割边 (注:输入后是连一条无向边,题目描述有误)
好,简单的割点和割边相信大家都会了,我们接下来来讲解边双连通分量和点双连通分量。
边双连通分量:
边双连通分量的概念: 对于一个无向图中的极大边双连通(没有割边)的子图,我们称这个子图为边双连通分量。
如果我们画一个图,再把割边标记出来,我们不难看出每个边双连通分量都被割边所分开,所以,求边双连通分量我们可以先求割边,再标记出来,在不走割边的情况下跑dfs,最后输出。
CODE:
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+10,maxm=4e6+10;
int b[maxm],n,m,cnt=1,ans,id,dfn[maxn],low[maxn],head[maxn],dcc[maxn];
struct edge{
int to,nxt;
}e[maxm];
vector<vector<int> >Ans;
void add(int f,int t){
e[++cnt].to=t;
e[cnt].nxt=head[f];
head[f]=cnt;
}
void tarjan(int node,int in_edge){
dfn[node]=low[node]=++id;
for (int i=head[node];i;i=e[i].nxt){
const int to=e[i].to;
if(dfn[to]==0){
tarjan(to,i);
if (dfn[node]<low[to])b[i]=b[i^1]=1;
low[node]=min(low[node],low[to]);
}
else if(i!=(in_edge^1))
low[node]=min(low[node],dfn[to]);
}
}
void dfs(int node,int ndcc){
dcc[node]=ndcc;
Ans[ndcc-1].push_back(node);
for (int i=head[node];i;i=e[i].nxt){
int to=e[i].to;
if(dcc[to]||b[i])continue;
dfs(to,ndcc);
}
}
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1;i<=m;i++){
int f,t;
cin>>f>>t;
if(f!=t)add(f,t),add(t,f);
}
for(int i=1;i<=n;i++)if(dfn[i]==0)tarjan(i,0);
for(int i=1;i<=n;i++)
if(dcc[i]==0){
Ans.push_back(vector<int>());
dfs(i,++ans);
}
cout<<ans<<"\n";
for(int i=0;i<ans;i++){
cout<<Ans[i].size()<<" ";
for(int j=0;j<Ans[i].size();j++)cout<<Ans[i][j]<<" ";
cout<<"\n";
}
}
这是模板题的代码,由于题目中的图不是简单图,有自环或者是重边,所以就用链式前向星了。
注:图有可能不连通,所以需要多次dfs
模板题:【模板】边双连通分量
点双连通分量:
首先,两个点双最多只有一个公共点,且这个点在这两个点双和它形成的子图中是割点。
因为当它们有两个及以上公共点时,它们可以合并为一个新的点双。
所以,割点属于多个点双。
我们这里要使用栈,首先按照dfs遍历顺序依次把节点弹入栈中,然后回溯的时候,若发现割点,那么就弹出栈中的节点,直至弹到此割点(注意,割点不弹出,因为割点属于多个点双连通分量)
然后弹出的一大堆东西就是点双连通分量。
注意,如果弹到根节点,那么不管它是不是割点,一律出栈。
这样子,我们就求出来了一个无向图中的所有点双。
(其实点双和scc强连通分量的思路有点像)
CODE:
#include<bits/stdc++.h>
using namespace std;
int n,m,low[500005],dfn[500005],ans,cnt,nxt[4000005],head[500005],go[4000005],k;
vector<int> dcc[500005];
stack<int>sta;
void add(int u,int v){
nxt[++k]=head[u];
head[u]=k;
go[k]=v;
}
void tarjan(int x,int root){
dfn[x]=low[x]=++cnt;
if(x==root&&!head[x])dcc[++ans].push_back(x);
sta.push(x);
for(int i=head[x];i;i=nxt[i]){
int g=go[i];
if(!dfn[g]){
tarjan(g,root);
low[x]=min(low[x],low[g]);
if(low[g]>=dfn[x]){
ans++;
int p;
do{
p=sta.top();
sta.pop();
dcc[ans].push_back(p);
}while(p!=g);
dcc[ans].push_back(x);
}
}
else low[x]=min(low[x],dfn[g]);
}
}
int main(){
cin>>n>>m;
for(int i=1,x,y;i<=m;i++){
cin>>x>>y;
if(x==y)continue;
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,i);
cout<<ans<<"\n";
for(int i=1;i<=ans;i++){
cout<<dcc[i].size()<<" ";
for(int j=0;j<dcc[i].size();j++)cout<<dcc[i][j]<<" ";
cout<<"\n";
}
}
那么点双有什么用处呢?
我们可以用它来搭建圆方树,来解决一下查询的问题,这里就不一一赘述了。