Tarjan求边双连通分量
更新日志
Update2024/10/25 为了与点双格式统一,并且原来的方法太慢,所以更新算法,原方法统一放入省略块中。使用求割边方法
前言
这种方法太慢,并且点双无法用求割点算法计算,不推荐,故而放入省略块中,主要用于讲解求割边算法的运用,以及不浪费我写了这么久的笔记。
思路
首先,我们求出原图中所有的桥,然后跑DFS
给原图分区。
求桥具体过程:Tarjan求桥
更具体的,我们遍历每一个点,假如这个点没有被分区,那么就从这个点开始深搜。
每一次深搜,都走不是桥的边,那么走到的就都属于一个边双。(很容易证明)
这样,我们把每一次深搜走到的所有点分成一个边双,储存相应信息即可。
细节
提醒一个小点吧,如果你和我一样使用原边编号储存所有桥(详见求桥部分),那么在判断的时候也应该使用原无向边编号。
模板
注:dfs函数的使用详见例题代码
int dcnt;
int dfn[N],low[N];
bool bri[M];
void tarjan(int now,int fed){
dfn[now]=low[now]=++cnt;
for(int e=hd[now];e;e=ne[e]){
int nxt=to[e],eid=(e-1)/2+1;
if(!dfn[nxt]){
tarjan(nxt,eid);
low[now]=min(low[now],low[nxt]);
if(low[nxt]>dfn[now]){
bri[eid]=true;
}
}else if(eid!=fed)low[now]=min(low[now],dfn[nxt]);
}
}
int bcnt;
int bcc[N];
veci nods[N];
void dfs(int now){
bcc[now]=bcnt;
nods[bcnt].push_back(now);
for(int e=hd[now];e;e=ne[e]){
int nxt=to[e],eid=(e-1)/2+1;
if(bcc[nxt]!=0||bri[eid])continue;
dfs(nxt);
}
}
例题
代码
前注:非题解,不做详细讲解
#include<bits/stdc++.h>
using namespace std;
typedef vector<int> veci;
const int N=5e5+5,M=2e6+5;
int n,m;
int cnt;
int hd[N],to[M*2],ne[M*2];
void adde(int u,int v){
to[++cnt]=v;
ne[cnt]=hd[u];
hd[u]=cnt;
}
int dcnt;
int dfn[N],low[N];
bool bri[M];
void tarjan(int now,int fed){
dfn[now]=low[now]=++cnt;
for(int e=hd[now];e;e=ne[e]){
int nxt=to[e],eid=(e-1)/2+1;
if(!dfn[nxt]){
tarjan(nxt,eid);
low[now]=min(low[now],low[nxt]);
if(low[nxt]>dfn[now]){
bri[eid]=true;
}
}else if(eid!=fed)low[now]=min(low[now],dfn[nxt]);
}
}
int bcnt;
int bcc[N];
veci nods[N];
void dfs(int now){
bcc[now]=bcnt;
nods[bcnt].push_back(now);
for(int e=hd[now];e;e=ne[e]){
int nxt=to[e],eid=(e-1)/2+1;
if(bcc[nxt]!=0||bri[eid])continue;
dfs(nxt);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
int a,b;
for(int i=1;i<=m;i++){
cin>>a>>b;
adde(a,b);
adde(b,a);
}
for(int i=1;i<=n;i++){
if(!dfn[i])tarjan(i,0);
}
for(int i=1;i<=n;i++){
if(!bcc[i]){
++bcnt;
dfs(i);
}
}
cout<<bcnt<<"\n";
for(int i=1;i<=bcnt;i++){
cout<<nods[i].size()<<" ";
for(auto j:nods[i])cout<<j<<" ";
cout<<"\n";
}
return 0;
}
思路
边双连通分量的定义是,在这个分量中,所有删去任意一条边,剩下的图仍然连通。
不难想到,割边就是分割了两个边双连通分量的边,我们可以通过找割边来获得强连通分量。(详见上方缩略方案,但是不推荐)
换种思路,我们不难想到,一个双连通分量与其它分量之间,必然只有至多一条边相连,那么我们可以考虑类似求强连通分量的方法。
具体的,我们只需要看一棵DFS生成树的子树是否只能通过当前节点与这棵子树的根节点的这条边与外界连通,也就是只有一条边与外界连通,即可判断这棵子树是否是一个双连通分量。
更详细的思路可以借鉴强连通分量部分。
细节
我们在DFS的过程中需要注意判断这条边是否和父节点走来的这条边是同一条无向边,之所以不判断是否是父节点,是因为可能会出现重边。
模板
int dcnt;
int dfn[N],low[N];
int scnt;
vector<int> scc[N];
stack<int> stk;
void tarjan(int x,int fed){
dfn[x]=low[x]=++dcnt;
stk.push(x);
for(int e=hd[x];e;e=ne[e]){
int nxt=to[e];
if(!dfn[nxt]){
tarjan(nxt,(e-1)/2+1);
low[x]=min(low[x],low[nxt]);
}else if(fed!=(e-1)/2+1)low[x]=min(low[x],dfn[nxt]);
}
if(low[x]>=dfn[x]){
++scnt;
while(1){
int tp=stk.top();stk.pop();
scc[scnt].push_back(tp);
if(tp==x)break;
}
}
}
例题
代码
前注:非题解,不做详细讲解
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5,M=2e6+5;
int n,m;
int cnt;
int hd[N],ne[M*2],to[M*2];
void adde(int a,int b){
to[++cnt]=b;
ne[cnt]=hd[a];
hd[a]=cnt;
}
int dcnt;
int dfn[N],low[N];
int scnt;
vector<int> scc[N];
stack<int> stk;
void tarjan(int x,int fed){
dfn[x]=low[x]=++dcnt;
stk.push(x);
for(int e=hd[x];e;e=ne[e]){
int nxt=to[e];
if(!dfn[nxt]){
tarjan(nxt,(e-1)/2+1);
low[x]=min(low[x],low[nxt]);
}else if(fed!=(e-1)/2+1)low[x]=min(low[x],dfn[nxt]);
}
if(low[x]>=dfn[x]){
++scnt;
while(1){
int tp=stk.top();stk.pop();
scc[scnt].push_back(tp);
if(tp==x)break;
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
int a,b;
for(int i=1;i<=m;i++){
cin>>a>>b;
adde(a,b);
adde(b,a);
}
for(int i=1;i<=n;i++){
if(!dfn[i])tarjan(i,0);
}
cout<<scnt<<"\n";
for(int i=1;i<=scnt;i++){
cout<<scc[i].size()<<" ";
for(auto j:scc[i])cout<<j<<" ";
cout<<"\n";
}
return 0;
}