Tarjan

这是一个解决图的问题十分有效的一个算法:Tarjan(塔扬算法)
它快在哪里?它可能将一个上十万个点的复杂图化为100个点以内的简单图。
这就引进了“强连通分量”(SCC)的概念:有向图中任意两点都连通的最大子图叫做强连通分量。
例如下图就是一个:
 1 → 2
↑↙↑ (G)
 3 → 4                    以2为例 2→1:2→3→1    2→3:2→3    2→4:2→3→4
你可以自己看看1,3,4 都可以与其它所以点连通                (注:1个点也算是一个强连通分量)
那么我们可以将G浓缩为一个点,因为它们内部是完全连通的。
而Tarjan则正是查找出这一个个强连通分量的利器(Θ(n+m))
Tarjan算法是一种基于dfs的算法,它一张图当做一棵搜索树来搜索,每一个强连通分量作为搜索树上的一个子树。
Tarjan算法中 我们定义:
dfn[x]:x点的时间戳,简单来说就是第几个被搜索到的,当然每个点的时间戳都不一样的,这就是一种很好的编号方式。
low[x]:x点所在的SCC中,它能回到的最早祖先的dfn值。
它的实现过程是这样的:(这里比较晦涩难懂,看到后面就好了。)
一、用栈来存储一些点,表示这些点即将各自被匹配在某一个SCC中,所以每一个新节点出现,就进栈。
二、当前节点的low和dfn都赋值为当前时间戳
三、若当前节点x有出度,继续深搜,那么这个点有2个情况:
1.已被访问,又有2种情况:
①在栈中,说明它还有加入当前要找到的SCC的可能,用它的dfn更新low[x],即low[x]=min(low[x],dfn[***])
②不在栈中,说明它已经找到了自己的SCC,没有价值了。
2.未被访问(当然不在栈中)
对它进行递归,回溯后用它的low更新low[x],即low[x]=min(low[x],low[***])
四、最后看当前节点的low是否还等于dfn,若等于,说明它是一个搜索树的根节点,那么它及栈中以上的节点均构成1个SCC。


那么先带着疑惑看我模拟一遍算法,再做解释:
写了一个模板题:
【题目描述】
求有向图的强连通分量
【输入格式】
第一行两个正整数n,m(1<=n,m<=10000),分别表示点数及边数
第二行到第m+1行,每行2个正整数u,v(1<=u,v<=n),表示1条有向边u→v
【输出格式】
共x行
x表示强连通分量的个数。
每行一组强连通分量
格式:{元素1,元素2……元素k}
每个元素按升序排列
【输入样例#1】
6 8
1 2
3 4
2 4
3 5
5 6
1 3
4 1
4 6
【输出样例#1】
{1,2,3,4}
{5}
{6}
【输入样例#1】
12 17
5 7
1 2
2 5
6 3
7 8
2 3
2 4
5 2
11 12
7 10
6 8
5 6
3 6
9 7
10 9
8 11
12 10
【输出样例#2】
{1}
{2,5}
{3,6}
{4}
{7,8,9,10,11,12}
【备注】
样例1图像:
 1 →  3 → 5
 ↓ ↖  ↓      ↓
 2 →  4 → 6


样例2图像:
1 →    2   →  3
    ↙   ↓↑      ↓↑
  4       5   →  6
           ↓       ↓
           7   →  8
       ↗  ↘       ↘
      9  ← 10        11
                   ↖   ↙
                       12


就用样例一来模拟吧。


dfn:  dfn:   dfn:
low:  low:  low:
 1 →  3 → 5
 ↓  ↖  ↓    ↓
 2 →  4 → 6
dfn:  dfn:   dfn:
low:  low:  low:


栈:NULL


先赋值当前节点x为1。按算法介绍二更新dfn,low,并深搜1(1→3→5→6)


dfn:1 dfn:2  dfn:3
low:1 low:2 low:3
 1 →  3 → 5
 ↓ ↖  ↓      ↓
 2 →  4 → 6
dfn:  dfn:   dfn:4
low:  low:  low:4


栈:
6
5
3
1


到6了以后,发现自己没有出度来dfs了!准备回溯前,发现自己dfn=low都是4,说明自己以及栈中在自己以上的构成了一个完整的强连通分量,所以我们得到了一个强连通分量:{6}


回溯到了5,根据算法介绍,low[5]=min(low[5],low[6]),还是3;它的所有出度也都遍历完了,回溯前发现自己的dfn=low,所以也找出了一个SCC:{5}(同上面对6的操作),并回溯到3。


dfn:1 dfn:2  dfn:3
low:1 low:2 low:3
 1 →  3 → 5
 ↓  ↖ ↓      ↓
 2 →  4 → 6
dfn:  dfn:   dfn:4
low:  low:  low:4


栈:
3
1


这下3还有一个出度:3→4!发现4还未被访问,所以继续搜4,并将4入栈。
dfn:1 dfn:2  dfn:3
low:1 low:2 low:3
 1 →  3 → 5
 ↓  ↖ ↓      ↓
 2 →  4 → 6
dfn:  dfn:5  dfn:4
low:  low:5 low:4
栈:
4
3
1


4的第一边是4→6,它正兴致勃勃要去dfs6,发现6已被访问了,在一看栈,原来6不在栈中,说明6已经不可能属于当前的SCC了,所以在看下一条:4→1,这下就今非昔比了,1在栈中!当然要更新了!low[4]=min(low[4],dfn[1])=1。回溯前判断,low=1,dfn=5,low≠dfn。回溯到3。
去更新low[3]=min(low[3],low[4])=1。回溯前判断low≠dfn。回溯到1。


dfn:1 dfn:2  dfn:3
low:1 low:1 low:3
 1 →  3 → 5
 ↓  ↖  ↓     ↓
 2 →  4 → 6
dfn:  dfn:5  dfn:4
low:  low:1 low:4
栈:
4
3
1


1找到了下一条边1→2,2没有被遍历,继续dfs。到了4,发现4被遍历了,且在栈中,low[2]=min(low[2],dfn[4])=5,判断low≠dfn,回溯到1。


dfn:1 dfn:2  dfn:3
low:1 low:1 low:3
 1 →  3 → 5
 ↓  ↖  ↓    ↓
 2 →  4 → 6
dfn:  dfn:5  dfn:4
low:  low:1 low:4
栈:
2
4
3
1


1也没有出度了,以判断,咦!low[1]=dfn[1]=1,1在栈中以上的的所有构成一个SCC:{1,2,3,4}




这下以1为起点的Tarjan就完了。
找到了3个SCC:{1,2,3,4},{5},{6}
提示:对于这个样例,一次Tarjan就可以把整张图的SCC都找出来了,但是,实际上还要继续遍历每个点,如果没被访问再让x等于那个点,再Tarjan。


对于算法的过程大家应该懂了,但是可能有一些道理没想通(如果你认为你懂了,可以跳过)
1.为什么说x出了栈,就不会和其它的强联通分量掺和了?
答:只要这个节点出栈,那就说明它找到了自己的强连通分量,如果还和其它点也是强连通分量,那么那些点也和x找到的强连通分量构成整个大的强连通分量,所以这与“它找到了自己的强连通分量”矛盾,所以不可能。
2.为什么被访问过(且在栈中)的点用dfn更新x,没被访问过的点回溯到x时用low更新x?
答:从定义来说,都用low来更新好像更好(不能都用dfn更新),其实这都是一样的,访问过(且在栈中)的点用low,dfn更新x都正确,用dfn来仅仅是为了与割点割边里的代码保持一致而已,显得更方便好看。

看一看代码吧

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<ctime>
#include<cctype>
#include<iomanip>
#include<algorithm>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
using namespace std;
int const maxn=10001,maxm=10001;
int n,m;
struct Edge{int u,v,nxt;}e[maxm]; int fst[maxn];//邻接表 
int dfn[maxn],low[maxn],id[maxn];//id[x]为x属于哪个强连通分量,强连通分量的编号定义在这里是搜索树的根的编号 
stack<int>num;  int ins[maxn];//in stack
void add(int a,int b,int i)//临接表操作 
{
	e[i].nxt=fst[a];
	fst[a]=i;
	e[i].u=a;e[i].v=b;
}
int cnt;//计数器 
int now;//当前 
void Tarjan(int x)
{
	int to;
	num.push(x);ins[x]=true;//入栈
	dfn[x]=low[x]=++cnt;//计数
	for(int k=fst[x];k;k=e[k].nxt)//邻接表遍历以u为起点的每一条边,k是边的编号
	{
		to=e[k].v; 
		if(dfn[to])//曾被访问过
		{ //这个括号不能省!否则有if else匹配问题 
			if(ins[to])//在栈中,还有意义
				low[x]=min(low[x],dfn[to]);//更新
		}
		else//未被访问过 
			Tarjan(to),low[x]=min(low[x],low[to]);//先递归一次算法,回溯后再更新
	}
	if(dfn[x]==low[x])//是根,取出x及栈中再x以上的所有元素,为同一个强连通分量 
		do	//取x以上的所有元素,并赋予它们的id为x 
		{
			now=num.top();//当前第一个 
			id[now]=x;//加入这个强连通分量 
			ins[now]=false;num.pop();//弹出 
		} 
		while(now!=x);//还没到x,继续循环 
	return ;
}
void output(int i)
{
	int x=id[i];
	printf("{");
	for(int j=i;j<=n;j++)
		if(id[j]==x)
			printf("%d,",j),id[j]=0;//0表示已输出 
	printf("\b}\n");
			
}
int main()
{
	//freopen("tarjan.in","r",stdin);
	//freopen("tarjan.out","w",stdout);
	scanf("%d%d",&n,&m);
	int a,b;
	for(int i=1;i<=m;i++) scanf("%d%d",&a,&b),add(a,b,i);//输入 
	for(int i=1;i<=n;i++) //遍历每个点 
		if(!dfn[i]) //未被访问过 
			Tarjan(i);//执行算法
	for(int i=1;i<=n;i++)//输出,略略看看吧 
		if(id[i])
			output(i);
	return 0;
} 
语种:C++
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值