Kosaraju算法

43 篇文章 1 订阅

最关键通用部分:强连通分量一定是图的深搜树的一个子树。

 Kosaraju算法

1.      算法思路

基本思路:

这个算法可以说是最容易理解,最通用的算法,其比较关键的部分是同时应用了原图G和反图GT。(步骤1)先用对原图G进行深搜形成森林(树),(步骤2)然后任选一棵树对其进行深搜(注意这次深搜节点A能往子节点B走的要求是EAB存在于反图GT),能遍历到的顶点就是一个强连通分量。余下部分和原来的森林一起组成一个新的森林,继续步骤2直到 没有顶点为止。

改进思路:

当然,基本思路实现起来是比较麻烦的(因为步骤2每次对一棵树进行深搜时,可能深搜到其他树上去,这是不允许的,强连通分量只能存在单棵树中(由开篇第一句话可知)),我们当然不这么做,我们可以巧妙的选择第二深搜选择的树的顺序,使其不可能深搜到其他树上去。想象一下,如果步骤2是从森林里选择树,那么哪个树是不连通(对于GT来说)到其他树上的呢?就是最后遍历出来的树,它的根节点在步骤1的遍历中离开时间最晚,而且可知它也是该树中离开时间最晚的那个节点。这给我们提供了很好的选择,在第一次深搜遍历时,记录时间i离开的顶点j,即numb[i]=j。那么,我们每次只需找到没有找过的顶点中具有最晚离开时间的顶点直接深搜(对于GT来说)就可以了。每次深搜都得到一个强连通分量。 

隐藏性质:

    分析到这里,我们已经知道怎么求强连通分量了。但是,大家有没有注意到我们在第二次深搜选择树的顺序有一个特点呢?如果在看上述思路的时候,你的脑子在思考,相信你已经知道了!!!它就是:如果我们把求出来的每个强连通分量收缩成一个点,并且用求出每个强连通分量的顺序来标记收缩后的节点,那么这个顺序其实就是强连通分量收缩成点后形成的有向无环图的拓扑序列。为什么呢?首先,应该明确搜索后的图一定是有向无环图呢?废话,如果还有环,那么环上的顶点对应的所有原来图上的顶点构成一个强连通分量,而不是构成环上那么多点对应的独自的强连通分量了。然后就是为什么是拓扑序列,我们在改进分析的时候,不是先选的树不会连通到其他树上(对于反图GT来说),也就是后选的树没有连通到先选的树,也即先出现的强连通分量收缩的点只能指向后出现的强连通分量收缩的点。那么拓扑序列不是理所当然的吗?这就是Kosaraju算法的一个隐藏性质。

 

算法思路:

找到一个合理顺序,使得我们只需要按照这个顺序进行DFS遍历,那么每一次的DFS就可以使我们得到一个强连通分量(SCC)

若遍历顺序由B中顶点开始,则需经过两个DFS遍历,可顺利找到A和B两个SCC,

若从A中顶点开始,则只经过一次DFS遍历,无法找到两个SCC

因此关键是如何让DFS以正确的顺序开始遍历(即由B开始)

由图可知:

不管从A开始DFS,还是从B开始DFS,因为A到B有一条边,所以最后退出DFS的点一定在A上,若选最后退出DFS的点为始点(位于A中)并对上图的 反图 进行一次DFS,则可以得到图中的两个SCC。

由此得到以下算法步骤:

  1. 对有向图G做DFS(第一次),按退出DFS函数的顺序(B先退出,A后退出)将顶点加入数组finished[]中(此时深度大的顶点(B)在前,深度小的顶点(A)在后)

  2. 求G的反图Gr

  3. 对反图Gr按finished数组中从后到前的顺序做DFS(第二次),遍历结束便可得到有向图中的所有SCC

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
struct edge{
	int to,next;
}edge1[maxn],edge2[maxn];
//edge1是原图,edge2是逆图
int head1[maxn],head2[maxn];
bool mark1[maxn],mark2[maxn];
int tot1,tot2;
int cnt1,cnt2;
int st[maxn];//对原图进行dfs,点的结束顺序从小到大排列。
int belong[maxn];//每个点属于哪个联通分量
int num;//每个联通分量的个数
int setnum[maxn];//每个联通分量中点的个数

void addedge(int u,int v){
	edge1[tot1].to=v;edge1[tot1].next=head1[u];head1[u]=tot1++;
	edge2[tot2].to=u;edge2[tot2].next=head2[v];head2[v]=tot2++;
} 
void dfs1(int u){
	mark1[u]=true;
	for(int i=head1[u];i!=-1;i=edge1[i].next)
        if(!mark1[edge1[i].to])
            dfs1(edge1[i].to);
	st[cnt1++]=u;
}
void dfs2(int u){
	mark2[u]=true;
	num++;
	belong[u]=cnt2;
	
	for(int i=head2[u];i!=-1;i=edge2[i].next)
	    if(!mark2[edge2[i].to])
	        dfs2(edge2[i].to);
}
void solve(int n){//点编号从1开始
    memset(mark1,false,sizeof(mark1)); 
	memset(mark2,false,sizeof(mark2)); 
	cnt1=cnt2=0;
	for(int i=1;i<=n;i++)
	    if(!mark1[i])
	        dfs1(i);
	        
	for(int i=cnt1-1;i>=0;i--)
	    if(!mark2[st[i]]){
	    	num=0;
	    	dfs2(st[i]);
	    	setnum[cnt2++]=num;
		}
}
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int s,d;
		cin>>s>>d;
		addedge(s,d);
	}
	solve(1);
	return 0;
} 

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值