求强连通分量的tarjan算法Gabow算法

求强连通分量的tarjan算法

强连通分量:是有向图中的概念,在一个图的子图中,任意两个点相互可达,也就是存在互通的路径,那么这个子图就是强连通分量。(如果一个有向图的任意两个点相互可达,那么这个图就称为强连通图)。

如果u是某个强连通分量的根,那么:

1u不存在路径可以返回到它的祖先

2u的子树也不存在路径可以返回到u的祖先。

· 例如:

· 强连通分量。在一个非强连通图中极大的强连通子图就是该图的强连通分量。比如图中子图{1,2,3,5}是一个强连通分量,子图{4}是一个强连通分量。

tarjan算法的基础是深度优先搜索,用两个数组lowdfn,和一个栈。low数组是一个标记数组,记录该点所在的强连通子图所在搜索子树的根节点的dfn值,dfn数组记录搜索到该点的时间,也就是第几个搜索这个点的。根据以下几条规则,经过搜索遍历该图和对栈的操作,我们就可以得到该有向图的强连通分量。

算法规则:

· 数组的初始化:当首次搜索到点p时,DfnLow数组的值都为到该点的时间。

· 堆栈:每搜索到一个点,将它压入栈顶。

· 当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’不在栈中,plow值为两点的low值中较小的一个。

· 当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’在栈中,plow值为plow值和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=6DFN[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

#include

#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建立图

{

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 求每个节点的出度

{

for(j=first1[i];j!=-1;j=edge1[j].next)

outdegree[i]++;

}

sum=num=0;

for(i=0;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算法

来自NOCOW
跳转到: 导航, 搜索

[编辑] 求解有向图强连通分量的Gabow算法

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;
  }
}

你可能感兴趣的:(强连通分量)