Tarjan算法——求强连通分量

什么是强连通分量

强连通分量,是一个有向图的最大强连通子图(看起来好像没有什么解释效果…),好吧,强连通分量就是在一个有向图中,从任意一个点出发,最多可以走过的所有的点构成的一个点集,将其称之为强连通。强连通分量就是这些集合里面最大的那个。如下图:
在这里插入图片描述
2,3,5,就是强连通的,因为从二出发可以到达2,3,5的任意一个点。所以1,4,也是强连通的。我们不难发现,除了双向边以外,强连通都是环套环的形式。今天,我们的算法也会在这张图上展开。

算法详解

其实求强连通的tarjan算法的本质就是求环套环,我们将以邻接表为基础讲讲求强连通的tarjan算法。不懂邻接表的朋友看这里:邻接表存图法,那么这个算法就是要求环套环了嘛。

怎么求环套环呢?本算法中引入了时间戳这个概念,就是在进行dfs遍历(我相信看这个blog的朋友都会dfs把)的时候,遍历到的第一个点就打上1这个时间戳,第二个点就打上2这个时间戳。第n个点就打上n这个时间戳,我们把存每个点的时间戳的数组取名为dfn,则dfn[i]就是第i个点的时间戳。那么在dfs的时候,就可以免去记录此点是否走过的数组,而直接判断,凡是有时间戳的都是已经走过的点。

我们再使用一个数组color来染色,凡是在一个强连通分量里边的就把它染成一个相同的数字。比如2,3,5在一个强连通分量里面,那color[2],color[3],color[5]就把它赋值成一个数字(什么时候染色一会讲)。我们还需要一个数组low,每一个点都有自己的low,记录方法和dfn是一样的,每一个点的low初始是和dfn一样的,当我们dfs的时候,如果形成环路,那就必然会dfs到一个已经dfs过得点,而且这个点的时间戳还比自己小,那么就需要把那个已经dfs过的点的low拿来更新自己的low,然后一路递归地更新回去,这样,在这个环里dfs过的点就会有相同的low值。例如:上图中2 dfs 到3,3到5,至此,点2,3,5的low值都是自己的dfn,但是你会发现点5除了点2搜不到任何点了,那么就拿2的low更新自己的,然后递归回去,也一路更新回去,点3的用点5的更新…

然而这还是不能完成染色的,尽管在一个环里面的low都已经是一样的了。因此,我们创建一个栈。每搜到一个点,就把它加入栈中。什么时候退栈呢?比如我们从2开始搜索的,最后到了点5,然后2,3,5,都已经在栈中了,开始return了,于是点5退到了点2,然后会发现点2的low和dfn是一样的(因为大家其实都是拿点2来更新自己,所以2自己的没动),那就开始退栈。每退出一个元素,就把这个元素染色,直到把自己退掉。至此,染色完成,所有染成一样的点都在一个强连通分量里面。

实例模拟

我们就这个图进行一次模拟。
我们从点1开始深搜,如图,点1被打上时间戳(D)和low(L),并被加入栈中
在这里插入图片描述
打时间戳和low并入栈代码如下:

dfn[u]=low[u]=++cnt;
st[++top]=u;

其中u就是当前被深搜的点。
然后我们搜到了点二,如图:
在这里插入图片描述
点2也被打上时间戳和low,并且加入栈中,此时栈中有点1,2.

以此法,最后会这样:
在这里插入图片描述
此时栈中有:1 2 3 5

然后5搜到了2,但是2已经搜过了,而且2不在别的强连通分量里面,所以需要用2的low更新5的(对于5来说2就是下一个点,而邻接表结构体成员中恰恰会存u,v,也就是5到2,所以我们可以很方便的取到2的low值)。5的更新完毕后,就开始return了,return到3,也要用5的low更新3的。所以更新到2会是这样:
在这里插入图片描述
这里我给出深搜的代码和更新的代码(其实这两个本来就是一起写的):

for(int i=head[u];i;i=e[i].next){
		if(dfn[e[i].v]==0){
			tarjan(e[i].v);
			low[u]=min(low[u],low[e[i].v]);
		}
		else if(color[e[i].v]==0){
			low[u]=min(low[u],low[e[i].v]);
		}
}

其中,用2更新5的时候,其实是执行else if语句里面的更新,因为2走过了,所以dfn[e[2].v]不可能等于0,因此第一个if他根本不会进去的。所以才需要第二的if,else if(color[e[i].v]==0)用于判断其是否已经属于别的强连通分量了,被染色当然就不会等于0了。属于别人的我们不要,我们要单身的~~不要二手——
好了,现在又回到了点2,按我刚刚描述的,要开始退栈染色了兄弟。代码如下:

if(low[u]==dfn[u]){
		color[u]=++col;
		while(st[top]!=u){
			color[st[top]]=col;
			top--;
		} 
		top--;
}

这段代码紧跟在循环的下面(不在循环内部),这段代码只有在递归的时候才会触发,到了点2的时候(我们递归回到了点2),然后发现点2的low和时间戳都是一样的。所以就进入这个判断。一进入判断,先把当前的点更新一下,因为这个while是先判断后循环,所以到最后是不会染色自己的(一旦st[top]==u,就直接退出了,不再color[st[top]]=col了)。当然,你也可以尝试写do-while循环,这样就不需要事先给自己染色。col其实就是本次染色要染成的数字。while循环下面还有一个top----,目的是使自己——当前的点也可以退出栈,因为st[top]==u就退出了循环,也就没有top----了,原理和刚刚那个事先染色的差不多。

大家可能会有一个问题,递归回来的路上,都会先经过low[u]=min(low[u],low[e[i].v]);然后被上一个点更新,导致low和dfn不同,然后进入不了if(low[u]==dfn[u])这个判断,凭什么点2就可以呢?因为,我们一开始就是拿点2更新的,所以点2的确是走过了low[u]=min(low[u],low[e[i].v]);这个赋值,然而走过了之后自己还是自己啊,自己就是最小的。所以点2的low和dfn其实都还是原来的,而且还相等,所以…进入判断了。

点2的判断也结束后,就回到了点1,于是点1就搜到了点4,所以点4也被打上时间戳,计数变量cnt不会重置,所以点4的时间戳就是4,low也是4。如图:
在这里插入图片描述
然后点4又搜回了点1(这两个是双向的),但是点1已经是搜过的,所以执行else if那一句,把自己的low变成了点1的了,然后再return回去,直到深搜退出。最终会是酱紫:
在这里插入图片描述
下面我给出完整的代码和样例输入输出

样例输入样例输出
5 72
1 21
1 41
1 52
2 31
3 5
4 1
5 2
1
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxm=1001;
int p,s,a,b;
int dfn[maxm],low[maxm],cnt;//时间戳、最小戳、计数 
int st[maxm],top; 
int color[maxm],col;//染色、强连通分量个数 

struct edge{
	int u,v,next;
}e[maxm];
int head[maxm],js;

void addedge(int u,int v){
	e[++js].u=u;
	e[js].v=v;
	e[js].next=head[u];
	head[u]=js;
	return;
}

void tarjan(int u){
	dfn[u]=low[u]=++cnt;
	st[++top]=u;
	for(int i=head[u];i;i=e[i].next){
		if(dfn[e[i].v]==0){
			tarjan(e[i].v);
			low[u]=min(low[u],low[e[i].v]);
		}
		else if(color[e[i].v]==0){
			low[u]=min(low[u],low[e[i].v]);
		}
	}
	if(low[u]==dfn[u]){
		color[u]=++col;
		while(st[top]!=u){
			color[st[top]]=col;
			top--;
		} 
		top--;
	}
	return;
}

int main(){
	cin>>p>>s;
	for(int i=1;i<=s;i++){
		cin>>a>>b;
		addedge(a,b);
	}

	int start;
	cin>>start;
	tarjan(start);
	
	for(int i=1;i<=p;i++) cout<<color[i]<<endl;
	
	return 0;
} 

手累,大家多模拟模拟就一定可以学会哦…

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要实现强连通分量,可以使用深度优先搜索(DFS)算法。以下是一个用 Python 实现的示例代码: ```python from collections import defaultdict class Graph: def __init__(self, vertices): self.V = vertices self.graph = defaultdict(list) self.Time = 0 def addEdge(self, u, v): self.graph[u].append(v) def SCCUtil(self, u, low, disc, stackMember, st): disc[u] = self.Time low[u] = self.Time self.Time += 1 stackMember[u] = True st.append(u) for v in self.graph[u]: if disc[v] == -1: # 如果 v 还没被访问过,则递归访问它 self.SCCUtil(v, low, disc, stackMember, st) low[u] = min(low[u], low[v]) elif stackMember[v] == True: # 如果 v 在栈中,则更新 low[u] low[u] = min(low[u], disc[v]) w = -1 # 存储强连通分量的顶点 if low[u] == disc[u]: while w != u: w = st.pop() print(w, end=" ") stackMember[w] = False print() def SCC(self): disc = [-1] * (self.V) low = [-1] * (self.V) stackMember = [False] * (self.V) st = [] for i in range(self.V): if disc[i] == -1: self.SCCUtil(i, low, disc, stackMember, st) # 示例用法 g1 = Graph(5) g1.addEdge(1, 0) g1.addEdge(0, 2) g1.addEdge(2, 1) g1.addEdge(0, 3) g1.addEdge(3, 4) print("强连通分量为:") g1.SCC() ``` 这段代码首先定义了一个 `Graph` 类,其中 `addEdge` 方法用于添加边。`SCCUtil` 方法是实现强连通分量的核心函数。`SCC` 方法用于遍历图中所有的强连通分量并打印出来。 在示例中,我们创建了一个包含 5 个顶点的图,并添加了几条边。然后调用 `SCC` 方法来获取强连通分量。输出结果将显示强连通分量的顶点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值