Tarjan(强连通分量缩点、双联通分量缩点、割点、桥)
未完待续,周日前会完成
前置知识:连通图,有向图,无向图,有向无环图, Dfs \texttt{Dfs} Dfs, Dfs \texttt{Dfs} Dfs 树,子图的概念。
参考资料
https://baike.baidu.com/item/%E5%8F%8C%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/5339004?fr=aladdin#2
https://baike.baidu.com/item/%E5%BC%BA%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/7448759?fr=aladdin
https://www.cnblogs.com/ljk123-de-bo-ke/p/10888905.html
大纲
Tarjan 简介 \color{#000}\texttt{Tarjan 简介} Tarjan 简介
Tarjan \texttt{Tarjan} Tarjan 这个人发明了这四种本质一样的图论算法: 强连通分量缩点 \color{#520}\texttt{强连通分量缩点} 强连通分量缩点, 双连通分量缩点 \color{#250}\texttt{双连通分量缩点} 双连通分量缩点, 割点 \color{#526}\texttt{割点} 割点 和 桥 \color{#000}\texttt{桥} 桥。其中前二者只是有向图无向图的区别,而后二者只是点和边的区别。四个算法统称 Tarjan \texttt{Tarjan} Tarjan 算法。
强连通分量缩点 \color{#520}\texttt{强连通分量缩点} 强连通分量缩点
首先这是在有向图上的。
强连通分量
强连通分量是什么?第一次看百度百科解释得这么好:
有向图强连通分量:在有向图 G G G 中,如果两个顶点 v i v_i vi, v j v_j vj 间有一条从 v i v_i vi 到 v j v_j vj 的有向路径,同时还有一条从 v j v_j vj 到 v i v_i vi 的有向路径,则称两个顶点强连通。如果有向图 G G G 的每两个顶点都强连通,称 G G G 是一个强连通图。有向图的极大强连通子图,称为强连通分量。
补充:强连通子图就是每两个节点都强连通的子图。
如上图:
( 2 , 4 , 5 ) (2,4,5) (2,4,5) 是一个强连通分量。
( 1 , 2 , 4 , 5 ) (1,2,4,5) (1,2,4,5) 不是一个强连通分量,因为 2 , 4 , 5 2,4,5 2,4,5 到 1 1 1 没有有向路径。
( 2 , 5 ) (2,5) (2,5) 不是强连通分量,因为这个强连通子图不是极大的( ( 2 , 4 , 5 ) (2,4,5) (2,4,5) 明显更大)。
( 3 , 6 ) (3,6) (3,6) 是一个强连通分量。
( 1 ) (1) (1) 是一个强连通分量,因为 1 1 1 不能跟别的点强连通。
所以上图有 3 3 3 个强连通分量: ( 2 , 4 , 5 ) (2,4,5) (2,4,5) 和 ( 3 , 6 ) (3,6) (3,6) 和 ( 1 ) (1) (1)。每个点都在一个强连通分量里面。
Tarjan \texttt{Tarjan} Tarjan实现
对于图 G G G 的每个联通子图 G k G_k Gk,先找一个点当做根节点用 Dfs \texttt{Dfs} Dfs 构造出 Dfs \texttt{Dfs} Dfs 树(就是 Dfs \texttt{Dfs} Dfs 整个子图,把 Dfs \texttt{Dfs} Dfs 走过的边当做树边,剩下的边是回溯边)。
上图以 1 1 1 为跟构建出的 Dfs \texttt{Dfs} Dfs 树(虚线是回溯边,实线是树边):
然后用 d f n i dfn_i dfni 表示整棵 Dfs \texttt{Dfs} Dfs 树每个节点 i i i 的 Dfs \texttt{Dfs} Dfs 序(即被 Dfs \texttt{Dfs} Dfs 访问到的顺序),用 l o w i low_i lowi 表示节点 i i i 及它的子树通过回溯边所有可到达的点 j j j 的 min { d f n j } \min\{dfn_j\} min{dfnj}(可以直观的认为 d f n dfn dfn 越小深度越小吧)。如下:
注: l o w low low 一样不一定就是同一个强连通分量。
然后在构造 Dfs \texttt{Dfs} Dfs 树的同时把所有东西都搞定。每次 Tarjan(x) \texttt{Tarjan(x)} Tarjan(x)(本质就是 Dfs \texttt{Dfs} Dfs 函数)就把 x x x 丢进栈 s t st st 里(用 v i s x vis_x visx 表示 x x x 是否在栈中),然后走以 x x x 为起点的边 ( x , y ) (x,y) (x,y):
- 如果 d f n y = = 0 dfn_y==0 dfny==0 说明 y y y 未遍历,就先 Tarjan(y) \texttt{Tarjan(y)} Tarjan(y),然后 l o w x = min { l o w x , l o w y } low_x=\min\{low_x,low_y\} lowx=min{lowx,lowy}。
- 如果 d f n y ≠ 0 & & v i s y dfn_y\neq0\&\&vis_y dfny=0&&visy 说明 x x x 自己可以通过回溯边到达深度更低的节点,那么就 l o w x = min { l o w x , d f n y } low_x=\min\{low_x,dfn_y\} lowx=min{lowx,dfny}。
然后很明显如果 l o w x = = d f n x low_x==dfn_x lowx==dfnx 就说明 x x x 及它的子树不能到达深度比 x x x 更低的节点了,说明 x x x 所在的强连通分量就在 x x x 的子树中。然后栈顶剩下的点直到 x x x 就正好是一个强连通分量。然后把这些弹出栈
因为 G G G 有很多个联通子图,所以循环找还没有 Dfs \texttt{Dfs} Dfs 序的节点 Tarjan \texttt{Tarjan} Tarjan。如下代码是给有向图 G G G 缩点,输出连通块个数、每个连通块的节点数、每个节点的连通块。
code
#include <bits/stdc++.h>
using namespace std;
//&Start
#define lng long long
#define lit long double
#define kk(i,n) "\n "[i<n]
const int inf=0x3f3f3f3f;
const lng Inf=1e17;
const int N=100010;
int n,m;
vector<int> e[N];
//&Tarjan
int ind,dfn[N],low[N];
int tem[N],cnt,tp,st[N],sum[N];
bool vis[N];
void Clear(){
ind=cnt=tp=0;
for(int i=1;i<=n;i++)
e[i].clear(),low[i]=dfn[i]=tem[i]=sum[i]=st[i]=vis[i]=0;
}
void Tarjan(int x){
dfn[x]=low[x]=++ind,vis[x]=1,st[++tp]=x;
for(auto to:e[x])
if(!dfn[to]) Tarjan(to),low[x]=min(low[x],low[to]);
else if(vis[to]) low[x]=min(low[x],dfn[to]);
if(dfn[x]==low[x]){
int tmp=-1; ++cnt;
while(tmp!=x) tmp=st[tp--],tem[tmp]=cnt,vis[tmp]=0,sum[cnt]++;
}
}
//&Main
int main(){
while(~scanf("%d%d",&n,&m)){
Clear();
for(int i=1,u,v;i<=m;i++)
scanf("%d%d",&u,&v),e[u].push_back(v);
for(int i=1;i<=n;i++)if(!dfn[i]) Tarjan(i);
printf("%d\n",cnt);
for(int i=1;i<=cnt;i++) printf("%d%c",sum[i],kk(i,cnt));
for(int i=1;i<=n;i++) printf("%d%c",tem[i],kk(i,n));
}
return 0;
}
双连通分量缩点 \color{#250}\texttt{双连通分量缩点} 双连通分量缩点
这就相当于无向图中的强连通分量。以下给出两种从不同角度讲述的定义。
定义①:
双连通分量又分点双连通分量和边双连通分量两种。若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图。一个无向图中的每一个极大点(边)双连通子图称作此无向图的点(边)双连通分量。
定义②:
对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通);如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。点双连通图的定义等价于任意两条边都同在一个简单环中,而边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量),边双连通的极大子图称为边双连通分量。
缩点 \color{#034}\texttt{缩点} 缩点
缩点就是把一个强联通分量看成一个点。只需要求出强连通分量后为每个强连通分量编号,然后保留原来的边中连接两个强连通分量的连在现在的两个强连通分量上。
割点 \color{#526}\texttt{割点} 割点
桥 \color{#000}\texttt{桥} 桥
#include <bits/stdc++.h>
using namespace std;
//&Start
#define lng long long
#define lit long double
#define kk(i,n) "\n "[i<n]
const int inf=0x3f3f3f3f;
const lng Inf=1e17;
const int N=60;
int n,m,ans;
vector<int> e[N];
//&Tarjan
int ind,dfn[N],low[N];
void Clear(){
ind=ans=0;
memset(dfn,0,sizeof dfn);
memset(low,0,sizeof low);
for(int i=1;i<=n;i++) e[i].clear();
}
void Tarjan(int x,int f){
dfn[x]=low[x]=++ind;
for(auto to:e[x])
if(!dfn[to]){
Tarjan(to,x),low[x]=min(low[x],low[to]);
if(low[to]>dfn[x]) ans++;
} else if(to!=f) low[x]=min(low[x],dfn[to]);
}
//&Main
int main(){
while(~scanf("%d%d",&n,&m)){
Clear();
for(int i=1,u,v;i<=m;i++){
scanf("%d%d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
for(int i=1;i<=n;i++)if(!dfn[i]) Tarjan(i,0);
printf("%d\n",ans);
}
return 0;
}