关于图论中Tarjan算法的一些总结

Tarjan算法求强连通分量

前置知识

1. 1. 有向图:一个只由有向边构成的图,Tarjan算法只适用于有向图。

2. 2. 强连通:

对于两个点 A , B A,B ,如果他们之间可以相互到达,那么就称点 A , B A,B 强联通。

对于一个图 G G ,如果其任意两个顶点都是强联通的,那么这个图就是一个强联通图。

对于一个非强联通图 G G ,如果其某一子图 G G' 为强联通图,那么 G G' 就被称为图 G G 的强连通分量。

算法实现

先来看一些定义(下图摘自oi-wiki):

右图叫做左图的dfs生成树。

而有向图的dfs生成树有 4 4 种边:

树边(tree edge):绿色边,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。

反祖边(back edge):黄色边,也被叫做回边,即指向祖先结点的边。

横叉边(cross edge):红色边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先时形成的。

前向边(forward edge):蓝色边,它是在搜索的时候遇到子树中的结点的时候形成的。

下面给出一个详细解释:

1. 1. 树边

在搜索未被访问过的点时经过的边,即在下面的代码中,如果我们搜到了点 u u ,且对于一条边 ( u , v ) (u,v) v 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. 反祖边

由于递归本质上是在一个栈中进行的,我们搜索一个点时将其压入栈,结束时将其弹出。如果我们搜到了点 u u ,且对于一条边 ( u , v ) (u,v) v 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) 来说,如果对于 u n u_n 的一条出边 ( u n , v ) (u_n,v) v u 1 , u 2 , . . . , u n 1 v是u_1,u_2,...,u_{n-1} 中的一个,那么该边即为一条反祖边。

3. 3. 横叉边

如果我们搜到了点 u u ,且对于一条边 ( u , v ) (u,v) v 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) 来说,如果对于 u n u_n 的一条出边 ( u n , v ) (u_n,v) v 访 u 1 , u 2 , . . . , u n 1 v已被访问过,但不是u_1,u_2,...,u_{n-1} 中的一个,那么该边即为一条横叉边。

4. 4. 前向边

当我们递归完一个结点 u u 的一条边指向的点 v v 的一个可以到达的点的集合(即一棵搜索树)时,如果 u u 的另外一条边直接指向该集合中的点,那么该边被称为一条前向边。

找到强连通分量的方法

首先有一个性质:我们假设 u u 是某一个强连通分量在搜索树中第一个访问到的结点,那么强连通分量一定存在于以 u u 为根的子树中。

证明:我们假设某一点 v v 在该强连通分量中但不在以 u u 为根的子树里,那么由于 u , v u,v 连通,那么从 u u v v 的路径里一定存在一条不在子树里的边。根据上文的定义,该边一定为一条横叉边或返祖边,那么 v v 已经被访问过,与 u u 是第一个访问到的结点矛盾,故上述结论成立。

tarjan算法

定义两个数组: d f n [ N ] , l o w [ N ] dfn[N],low[N]

d f n [ u ] dfn[u] 表示点 u u 被搜索到时的时间戳。

l o w [ u ] low[u] 表示点 u u 通过一些边能够到达的搜索栈里的最早的时间戳。

其中 d f n [ u ] dfn[u] 是在一开始就已确定,不再改变吗,那么下面我们着重讨论如何更新 l o w low

对于一个点 u u ,我们考虑它自己的另外没有被搜索过的边,如果 v v 被搜索过且 d f n [ v ] dfn[v] 小于 d f n [ u ] dfn[u] ,那么 v v 的访问时间一定比 u u 早,更新 l o w [ u ] low[u]

然后考虑 u u 子树中的结点 v v ,如果 v v 可以到达一个点,且该点的 d f n dfn 小于 d f n [ u ] dfn[u] ,且在搜索栈里,那么该点必定在 u u 之前被搜索到并且 u u 可以通过 v v 来到达该点。

如果点 v v 不在搜索栈里,那么 v v 所在的强连通分量一定被处理过,所以我们不考虑。


我们来看这样一张图,箭头代表了每个点的 l o w low 值(边的方向为

该图中只有一个点 d f n = l o w dfn=low ,也就是 1 1 号点。由于上面的点能够到达任意一个下面的点,所以如果下面的点 l o w low 可以到达上面的点,那么这些点可以构成一个强连通分量。

当一个点的 l o w = d f n 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 值大于等于该点的 d f n 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 更后面的结点,即low[v]>dfn[u],对割点程序稍加改动即可。

割点和割边对有向图和无向图均适用

双连通分量

强连通分量是关于有向图的,那么对于无向图有没有上述性质呢?

在一张连通的无向图中,对于两个点 u u v v ,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 u u v v 边双连通 。

在一张连通的无向图中,对于两个点 u u v v ,如果无论删去哪个点(只能删去一个,且不能删 u u v v 自己)都不能使它们不连通,我们就说 u u v v 点双连通 。

双连通分量的定义类似于强连通分量的定义。

求出双连通分量

方法很简单。

割点是点双连通分量的交点。

割边连接两个边双连通分量。

求出割点和割边,然后进行缩点即可。

几个重要性质

一个割点同时属于多个点双连通分量。

边双连通分量和割边共同组成一棵树。

发布了45 篇原创文章 · 获赞 18 · 访问量 4754

猜你喜欢

转载自blog.csdn.net/wljoi/article/details/102383765