Tarjan算法求强连通分量
前置知识
1. 1. 1.有向图:一个只由有向边构成的图,Tarjan算法只适用于有向图。
2. 2. 2.强连通:
对于两个点 A , B A,B A,B,如果他们之间可以相互到达,那么就称点 A , B A,B A,B强联通。
对于一个图 G G G,如果其任意两个顶点都是强联通的,那么这个图就是一个强联通图。
对于一个非强联通图 G G G,如果其某一子图 G ′ G' G′为强联通图,那么 G ′ G' G′就被称为图 G G G的强连通分量。
算法实现
先来看一些定义(下图摘自oi-wiki):
右图叫做左图的dfs生成树。
而有向图的dfs生成树有 4 4 4种边:
树边(tree edge):绿色边,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
反祖边(back edge):黄色边,也被叫做回边,即指向祖先结点的边。
横叉边(cross edge):红色边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先时形成的。
前向边(forward edge):蓝色边,它是在搜索的时候遇到子树中的结点的时候形成的。
下面给出一个详细解释:
1. 1. 1.树边
在搜索未被访问过的点时经过的边,即在下面的代码中,如果我们搜到了点 u u u,且对于一条边 ( u , v ) (u,v) (u,v), v v v点未被访问过,那么边 ( u , v ) (u,v) (u,v)即为一条树边。
所有的树边构成一棵搜索树。
void dfs(int u){
for(int e=first[u];e;e=nxt[e]){
int v=to[e];
if(!vis[v]) dfs(v);
}
}
2. 2. 2.反祖边
由于递归本质上是在一个栈中进行的,我们搜索一个点时将其压入栈,结束时将其弹出。如果我们搜到了点 u u u,且对于一条边 ( u , v ) (u,v) (u,v), v v v点还在搜索栈中,那么边 ( u , v ) (u,v) (u,v)即为一条反祖边。
更通俗一点来说,对于在一次dfs中搜到的边 ( u 1 , u 2 ) , ( u 2 , u 3 ) , . . . , ( u n − 1 , u n ) (u_1,u_2),(u_2,u_3),...,(u_{n-1},u_n) (u1,u2),(u2,u3),...,(un−1,un)来说,如果对于 u n u_n un的一条出边 ( u n , v ) (u_n,v) (un,v), v 是 u 1 , u 2 , . . . , u n − 1 v是u_1,u_2,...,u_{n-1} v是u1,u2,...,un−1中的一个,那么该边即为一条反祖边。
3. 3. 3.横叉边
如果我们搜到了点 u u u,且对于一条边 ( u , v ) (u,v) (u,v), v v v点已被访问过但不在搜索栈中,那么边 ( u , v ) (u,v) (u,v)即为一条横叉边。
更通俗一点来说,对于在一次dfs中搜到的边 ( u 1 , u 2 ) , ( u 2 , u 3 ) , . . . , ( u n − 1 , u n ) (u_1,u_2),(u_2,u_3),...,(u_{n-1},u_n) (u1,u2),(u2,u3),...,(un−1,un)来说,如果对于 u n u_n un的一条出边 ( u n , v ) (u_n,v) (un,v), v 已 被 访 问 过 , 但 不 是 u 1 , u 2 , . . . , u n − 1 v已被访问过,但不是u_1,u_2,...,u_{n-1} v已被访问过,但不是u1,u2,...,un−1中的一个,那么该边即为一条横叉边。
4. 4. 4.前向边
当我们递归完一个结点 u u u的一条边指向的点 v v v的一个可以到达的点的集合(即一棵搜索树)时,如果 u u u的另外一条边直接指向该集合中的点,那么该边被称为一条前向边。
找到强连通分量的方法
首先有一个性质:我们假设 u u u是某一个强连通分量在搜索树中第一个访问到的结点,那么强连通分量一定存在于以 u u u为根的子树中。
证明:我们假设某一点 v v v在该强连通分量中但不在以 u u u为根的子树里,那么由于 u , v u,v u,v连通,那么从 u u u到 v v v的路径里一定存在一条不在子树里的边。根据上文的定义,该边一定为一条横叉边或返祖边,那么 v v v已经被访问过,与 u u u是第一个访问到的结点矛盾,故上述结论成立。
tarjan算法
定义两个数组: d f n [ N ] , l o w [ N ] dfn[N],low[N] dfn[N],low[N]。
d f n [ u ] dfn[u] dfn[u]表示点 u u u被搜索到时的时间戳。
l o w [ u ] low[u] low[u]表示点 u u u通过一些边能够到达的搜索栈里的最早的时间戳。
其中 d f n [ u ] dfn[u] dfn[u]是在一开始就已确定,不再改变吗,那么下面我们着重讨论如何更新 l o w low low。
对于一个点 u u u,我们考虑它自己的另外没有被搜索过的边,如果 v v v被搜索过且 d f n [ v ] dfn[v] dfn[v]小于 d f n [ u ] dfn[u] dfn[u],那么 v v v的访问时间一定比 u u u早,更新 l o w [ u ] low[u] low[u]
然后考虑 u u u子树中的结点 v v v,如果 v v v可以到达一个点,且该点的 d f n dfn dfn小于 d f n [ u ] dfn[u] dfn[u],且在搜索栈里,那么该点必定在 u u u之前被搜索到并且 u u u可以通过 v v v来到达该点。
如果点 v v v不在搜索栈里,那么 v v v所在的强连通分量一定被处理过,所以我们不考虑。
我们来看这样一张图,箭头代表了每个点的
l
o
w
low
low值(边的方向为
↘
↘
↘)
该图中只有一个点 d f n = l o w dfn=low dfn=low,也就是 1 1 1号点。由于上面的点能够到达任意一个下面的点,所以如果下面的点 l o w low low可以到达上面的点,那么这些点可以构成一个强连通分量。
当一个点的 l o w = d f n low=dfn low=dfn,那么它必定无法回到上面的点,也就是它就是一个强连通分量的顶点。
Code HAOI2006受欢迎的牛
#include<bits/stdc++.h>
using namespace std;
int Read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
return x*f;
}
int low[100005],dfn[100005],vis[100005],ind=0;
int first[200005],nxt[200005],to[200005],tot=0;
int col[100005],sz[100005],cnt=0,n,m,cd[100005];
int X[200005],Y[200005];
void Add(int x,int y){
nxt[++tot]=first[x];
first[x]=tot;
to[tot]=y;
}
stack<int> s;
void tarjan(int u){
s.push(u);
vis[u]=1;
dfn[u]=low[u]=++ind;
for(int e=first[u];e;e=nxt[e]){
int v=to[e];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
cnt++;
int x;
do{
x=s.top();
s.pop();
col[x]=cnt;
sz[cnt]++;
vis[x]=0;
}while(x!=u);
}
}
int main(){
n=Read(),m=Read();
for(int i=1;i<=m;i++){
X[i]=Read(),Y[i]=Read();
Add(X[i],Y[i]);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i);
}
for(int i=1;i<=m;i++){
if(col[X[i]]!=col[Y[i]]){
cd[col[X[i]]]++;
}
}
int ans=0,ff=0;
for(int i=1;i<=cnt;i++){
if(cd[i]==0) ff++,ans+=sz[i];
}
if(ff>1) cout<<0<<endl;
else cout<<ans<<endl;
}
割点与割边
割点
定义:在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点。
还是上面这张图。
我们想象如果一个点的下面的点的 l o w low low值大于等于该点的 d f n dfn dfn,那么该点下面的点只能到达在该点之后搜索的点,即以该点为根节点的子树。这就意味着如果删掉那个点,这个图将不再连通。
所以我们求割点是只需要判断low[v]>=dfn[u]
,然后标记该点即可。
注意特判根节点是否有多于一个孩子,如果是,那根节点也是割点。
#include<bits/stdc++.h>
using namespace std;
inline int Read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
return x*f;
}
inline void Write(int x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9){
Write(x/10);
}
putchar(x%10+'0');
}
int first[200005],nxt[200005],to[200005],tot=0;
int dfn[200005],low[200005],ind=0,cut[200005];
inline void Add(int x,int y){
nxt[++tot]=first[x];
first[x]=tot;
to[tot]=y;
}
inline void tarjan(int u,int rt){
int child=0;
dfn[u]=low[u]=++ind;
for(int e=first[u];e;e=nxt[e]){
int v=to[e];
if(!dfn[v]){
tarjan(v,rt);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]&&u!=rt) cut[u]=1;
if(u==rt) child++;
}
low[u]=min(low[u],dfn[v]);
}
if(u==rt&&child>=2) cut[rt]=true;
}
int main(){
int n,m,ans=0;
n=Read(),m=Read();
for(int i=1;i<=m;i++){
int x=Read(),y=Read();
Add(x,y);
Add(y,x);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i,i);
}
for(int i=1;i<=n;i++){
if(cut[i]) ans++;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++){
if(cut[i]) cout<<i<<" ";
}
}
割边
定义大致同割点,一条边为割边的条件为割掉该边后使原图不再连通。
继续使用刚才的方法进行理解。
如果需要割掉一条边使得图不再连通,那么该边的另一个结点必定无法通过其他边来到达上面的结点,也就是以
u
u
u为根的子树里的点只能到达比
u
u
u更后面的结点,即low[v]>dfn[u]
,对割点程序稍加改动即可。
割点和割边对有向图和无向图均适用
双连通分量
强连通分量是关于有向图的,那么对于无向图有没有上述性质呢?
在一张连通的无向图中,对于两个点 u u u 和 v v v ,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 u u u 和 v v v 边双连通 。
在一张连通的无向图中,对于两个点 u u u 和 v v v ,如果无论删去哪个点(只能删去一个,且不能删 u u u 和 v v v 自己)都不能使它们不连通,我们就说 u u u 和 v v v 点双连通 。
双连通分量的定义类似于强连通分量的定义。
求出双连通分量
方法很简单。
割点是点双连通分量的交点。
割边连接两个边双连通分量。
求出割点和割边,然后进行缩点即可。
几个重要性质
一个割点同时属于多个点双连通分量。
边双连通分量和割边共同组成一棵树。