求强连通分量的tarjan算法
强连通分量:是有向图中的概念,在一个图的子图中,任意两个点相互可达,也就是存在互通的路径,那么这个子图就是强连通分量。(如果一个有向图的任意两个点相互可达,那么这个图就称为强连通图)。
如果u是某个强连通分量的根,那么:
(1)u不存在路径可以返回到它的祖先。
(2)u的子树也不存在路径可以返回到u的祖先。
· 例如:
· 强连通分量。在一个非强连通图中极大的强连通子图就是该图的强连通分量。比如图中子图{1,2,3,5}是一个强连通分量,子图{4}是一个强连通分量。
tarjan算法的基础是深度优先搜索,用两个数组low和dfn,和一个栈。low数组是一个标记数组,记录该点所在的强连通子图所在搜索子树的根节点的dfn值,dfn数组记录搜索到该点的时间,也就是第几个搜索这个点的。根据以下几条规则,经过搜索遍历该图和对栈的操作,我们就可以得到该有向图的强连通分量。
算法规则:
· 数组的初始化:当首次搜索到点p时,Dfn与Low数组的值都为到该点的时间。
· 堆栈:每搜索到一个点,将它压入栈顶。
· 当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’不在栈中,p的low值为两点的low值中较小的一个。
· 当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’在栈中,p的low值为p的low值和p’的dfn值中较小的一个。
· 每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的low值等于dfn值,则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。
· 继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。
算法伪代码:
tarjan(u)
{
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入 栈中
for each (u, v) in E // 枚举每一条边
if (!dfn[v]) // 如果节点v未被访问过
{
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
}
else if (v in S) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
do{
v = S.pop // 将v退栈,为该强连通分量中一个顶点
}while(u == v);
}
演示算法流程;
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。
应用例子:(OJ 1484 popular cows)
题意:有N只牛,输入a,b的话,则说明b关注a,而且关注有传递性。例如c关注b,且b关注a,则c也关注a。题目问有多少只奶牛能被其他所有的奶牛关注。
把题目的模型转换:N个顶点的有向图,有M条边。求一共有多少个点,满足这样的条件:所有其它的点都可以到达这个点。这个点满足条件的充要条件是:这个点是树中唯一的出度为0的点。
先求强连通分量,然后可以把强连通分量缩成一个点,因为,在强连通分量中的任意两个点可以到达,所有的点具有相同的性质,即它们分别能到达的点集都是相同的,能够到达它们的点集也是相同的。然后就重新构图,缩点后的图是没有强连通分量的,否则,可将环上所有点也缩成一个点,与极大强联通分量矛盾。所以只要找出度为0的点,并且出度为0的点只有1个 ,如果出度为0的点有多个的话,问题则无解。
代码:
#include<stdio.h>
#include<string.h>
#define adj 10010
#define edg 50010
struct node
{
int v;
int next;
};
node edge[edg];
node edge1[edg];
int low[adj],dfn[adj],Stack[adj];
int first[adj],first1[adj],fuck[adj];
bool ins[adj];
int n,m,temp,cnt,top,count;
int cnt_size[adj],belong[adj],outdegree[adj];
void creat(int u,int v)
{
edge1[count].next=first1[u];
edge1[count].v=v;
first1[u]=count++;
}
void tarjan(int u)
{
int i,v;
dfn[u]=low[u]=++temp;
Stack[top++]=u;
ins[u]=true;
for(i=first[u];i!=-1;i=edge[i].next)
{
v=edge[i].v;
if(!dfn[v])
{
tarjan(v);
if(low[u]>low[v])
low[u]=low[v];
}
else if(ins[v])
{
if(low[u]>dfn[v])
low[u]=dfn[v];
}
}
if(dfn[u]==low[u])
{
int j;
do
{
top--;
j=Stack[top];
ins[j]=false;
cnt_size[cnt]++;
belong[j]=cnt;
}while(u!=j);
cnt++;
}
}
int main()
{
int i,j,k,v,sum,num;
int e=0;
scanf("%d%d",&n,&m);
memset(first,-1,sizeof(first));
for(k=0;k<m;k++) 建立图
{
scanf("%d%d",&i,&j);
edge[e].v=j;
edge[e].next=first[i];
first[i]=e;
e++;
}
memset(dfn,0,sizeof(dfn));
memset(ins,false,sizeof(ins));
temp=cnt=top=0;
memset(cnt_size,0,sizeof(cnt_size));
memset(low,0,sizeof(low));
for(i=1;i<=n;i++) 求强连通分量
{
if(!dfn[i])
tarjan(i);
}
memset(first1,-1,sizeof(first1));
count=0;
for(i=1;i<=n;i++) 重新构造图
{
for(j=first[i];j!=-1;j=edge[j].next)
{
v=edge[j].v;
if(belong[i]!=belong[v])
creat(belong[i],belong[v]);
}
}
memset(outdegree,0,sizeof(outdegree));
for(i=0;i<cnt;i++) 求每个节点的出度
{
for(j=first1[i];j!=-1;j=edge1[j].next)
outdegree[i]++;
}
sum=num=0;
for(i=0;i<cnt;i++)
{
if(outdegree[i]==0) 求节点为0的个数
{
sum=cnt_size[i];
num++;
}
}
if(num==1)
printf("%d\n",sum);
else
printf("0\n");
return 0;
}
Gabow算法与Tarjan算法的核心思想实质上是相通的,就是利用强连通分量必定是DFS的一棵子树这个重要性质,通过找出这个子树的根来求解强分量.具体到实现是利用一个栈S来保存DFS遇到的所有树边的另一端顶点,在找出强分量子树的根之后,弹出S中的顶点一一进行编号. 二者不同的是,Tarjan算法通过一个low数组来维护各个顶点能到达的最小前序编号,而Gabow算法通过维护另一个栈来取代low数组,将前序编号值更大的顶点都弹出,然后通过栈顶的那个顶点来判断是否找到强分量子树的根
int Gabow(Graph G) { // 初始DFS用到的全局变量 S = StackInit(G->V); // S用来保存所有结点 P = StackInit(G->V); // P用来维护路劲 int v; for (v = 0; v < G->V; ++v) pre[v] = G->sc[v] = -1; cnt = id = 0; // DFS for (v = 0; v < G->V; ++v) if (pre[v] == -1) GabowDFS(G, v); // 释放栈空间 StackDestroy(S); StackDestroy(P); return id; // 返回id的值,这恰好是强连通分量的个数 } void GabowDFS(Graph G, int w) { Link t; int v; pre[w] = cnt++; // 对前序编号编号 StackPush(S, w); // 讲路径上遇到的树边顶点入栈 StackPush(P, w); for (t = G->adj[w]; t; t = t->next) { if (pre[v = t->v] == -1) // 如果当前顶点以前未遇到,则对其进行DFS GabowDFS(G, v); else if (G->sc[v] == - 1) // 否则如果当前顶点不属于强分量 while (pre[StackTop(P)] > pre[v]) // 就将路径栈P中大于当前顶点pre值的顶点都弹出 StackPop(P); } if (StackTop(P) == w) { // 如果P栈顶元素等于w,则找到强分量的根,就是w StackPop(P); do { v = StackPop(S); // 把S中的顶点弹出编号 G->sc[v] = id; } while (v != w); ++id; } }