这是一个解决图的问题十分有效的一个算法: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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
语种:C++