关于有向图的强连通分量

来源:互联网 发布:mac mini 音频输出 编辑:程序博客网 时间:2024/05/01 21:00

有向图的强连通分量一个有向图中,如果节点i能够通过一些边到达节点j,就简写成i能到达j。如果对于任意两个节点i,j均有i能到达j或j能到达i,则说此图是连通的。如果对于任意两个节点i,j均有i能到达j且j能到达i,则说此图是强连通的。对于一个无向图,说强联通没有意义,因为此时强连通就是连通。而对于一个有向图,它不一定是强连通的,但可以分为几个极大的强连通子图(“极大”的意思是再加入任何一个顶点就不满足强连通了)。这些子图叫做这个有向图的强连通分量。下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。



直接根据定义,用双向遍历取交集的方法求强连通分量,时间复杂度为O(N^2+M)。更好的方法是Kosaraju算法或Tarjan算法,两者的时间复杂度都是O(N+M)。本文介绍的是Tarjan算法。[Tarjan算法]Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,Low(u)=Min{DFN(u),Low(v),(u,v)为树枝边,u为v的父节点           DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)}当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。

接下来是对算法流程的演示。从节点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已经出栈,不再访问6,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。


继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=4。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。



至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是 O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。 在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。



Tarjan算法

感谢Faint.Wisdom讲解求最近公共祖先(LCA)的Tarjan算法!


[编辑]求最近公共祖先(LCA)的Tarjan算法

   首先,Tarjan算法是一种离线算法,也就是说,它要首先读入所有的询问(求一次LCA叫做一次询问),然后并不一定按照原来的顺序处理这些询问。而打乱这个顺序正是这个算法的巧妙之处。看完下文,你便会发现,如果偏要按原来的顺序处理询问,Tarjan算法将无法进行。
   Tarjan算法是利用并查集来实现的。它按DFS的顺序遍历整棵树。对于每个结点x,它进行以下几步操作:   * 计算当前结点的层号lv[x],并在并查集中建立仅包含x结点的集合,即root[x]:=x。   * 依次处理与该结点关联的询问。   * 递归处理x的所有孩子。   * root[x]:=root[father[x]](对于根结点来说,它的父结点可以任选一个,反正这是最后一步操作了)。

  现在我们来观察正在处理与x结点关联的询问时并查集的情况。由于一个结点处理完毕后,它就被归到其父结点所在的集合,所以在已经处理过的结点中(包括 x本身),x结点本身构成了与x的LCA是x的集合,x结点的父结点及以x的所有已处理的兄弟结点为根的子树构成了与x的LCA是father[x]的集合,x结点的父结点的父结点及以x的父结点的所有已处理的兄弟结点为根的子树构成了与x的LCA是father[father[x]]的集合……(上面这几句话如果看着别扭,就分析一下句子成分,也可参照右面的图)假设有一个询问(x,y)(y是已处理的结点),在并查集中查到y所属集合的根是z,那么z 就是x和y的LCA,x到y的路径长度就是lv[x]+lv[y]-lv[z]*2。累加所有经过的路径长度就得到答案。   现在还有一个问题:上面提到的询问(x,y)中,y是已处理过的结点。那么,如果y尚未处理怎么办?其实很简单,只要在询问列表中加入两个询问(x, y)、(y,x),那么就可以保证这两个询问有且仅有一个被处理了(暂时无法处理的那个就pass掉)。而形如(x,x)的询问则根本不必存储。   如果在并查集的实现中使用路径压缩等优化措施,一次查询的复杂度将可以认为是常数级的,整个算法也就是线性的了。

http://purety.jp/akisame/oi/TJU/


[编辑]求有向图的强连通分支(SCC)的Tarjan算法

求有向图的强连通分支的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量(割点、桥)的Tarjan算法,以及求最近公共祖先的Tarjan算法。

Tarjan算法是通过对原图进行一次DFS实现的。下面给出该算法的PASCAL语言模板:

procedure dfs(s:int);var ne:int;begin  view[s]:=1;                  //view[i]表示点i的访问状态.未访问,正访问,已访问的点,值分别为0,1,2  inc(top); stack[top]:=s;     //当前点入栈  inc(time); rea[s]:=time; low[s]:=time;  //记录访问该点的真实时间rea和最早时间low   ne:=head[s];  while ne<>0 do begin    if view[e[ne]]=0 then dfs(e[ne]);     //如果扩展出的点未被访问,继续扩展    if view[e[ne]]<2 then low[s]:=min(low[s],low[e[ne]]);    //如果扩展出的不是已访问的点,更新访问源点s的最早时间.容易理解,如果一个点能到达之前访问过的点,那么路径中存在一个环使它能更早被访问    ne:=next[ne];  endif rea[s]=low[s] then begin             //如果s的最早访问时间等于其实际访问时间,则可把其视作回路的"始点"    inc(tot);                             //连通块编号    while stack[top+1]<>s do begin        //将由s直接或间接扩展出的点标记为同一连通块,标记访问后出栈      lab[stack[top]]:=tot;               //lab[i]表示点i所属的连通块      view[stack[top]]:=2;      dec(top);    end;  end;end;

图是用邻接表存储的,e[i]表示第i条边指向的点。

算法运行过程中,每个顶点和每条边都被访问了一次,所以该算法的时间复杂度为O(V+E)。

下面是求强连通分量的Tarjan算法的C++实现

#define  M 5010              //题目中可能的最大点数       int STACK[M],top=0;          //Tarjan 算法中的栈 bool InStack[M];             //检查是否在栈中 int DFN[M];                  //深度优先搜索访问次序 int Low[M];                  //能追溯到的最早的次序 int ComponentNumber=0;        //有向图强连通分量个数 int Index=0;                 //索引号 vector <int> Edge[M];        //邻接表表示 vector <int> Component[M];   //获得强连通分量结果int InComponent[M];   //记录每个点在第几号强连通分量里int ComponentDegree[M];     //记录每个强连通分量的度void Tarjan(int i) {     int j;     DFN[i]=Low[i]=Index++;     InStack[i]=true;     STACK[++top]=i;     for (int e=0;e<Edge[i].size();e++)     {         j=Edge[i][e];         if (DFN[j]==-1)         {             Tarjan(j);             Low[i]=min(Low[i],Low[j]);         }         else if (InStack[j])             Low[i]=min(Low[i],DFN[j]);     }     if (DFN[i]==Low[i])     {         ComponentNumber++;         do         {             j=STACK[top--];             InStack[j]=false;             Component[ComponentNumber].push_back(j);   InComponent[j]=ComponentNumber;        }         while (j!=i);     } } void solve(int N)     //N是此图中点的个数,注意是0-indexed! {     memset(STACK,-1,sizeof(STACK));     memset(InStack,0,sizeof(InStack));     memset(DFN,-1,sizeof(DFN));     memset(Low,-1,sizeof(Low));      for(int i=0;i<N;i++)         if(DFN[i]==-1)             Tarjan(i);    }

关于Tarjan算法的更为详细的讲解,可以在这里找到。

Tarjan的C++代码(STL):

#include <iostream>#include <cstdio>#include <cstring>#include <cstdlib>#include <algorithm>#include <list>#include <stack> using namespace std; const int kMaxN = 3001; class Graph { public:  Graph(int vertex_count = 0) {    vertex_count_ = vertex_count;    memset(degree_, 0, sizeof(degree_));  }  void insert_edge(int v, int w) {    graph_[v].push_back(w);    degree_[w]++;  }  void TarjanInit() {    tarjan_count = 0;    memset(tarjan_dfn, 0, sizeof(tarjan_dfn));    memset(tarjan_low, 0, sizeof(tarjan_low));    memset(tarjan_instack, false, sizeof(tarjan_instack));    for (int i = 1; i <= vertex_count_; i++) {      tarjan_set[i] = i;    }  }  void Tarjan(int v) {                  // Need TarjanInit()    tarjan_count++;    tarjan_dfn[v] = tarjan_count;    tarjan_low[v] = tarjan_count;    tarjan_stack.push(v);    tarjan_instack[v] = true;     for (list<int>::const_iterator i = graph_[v].begin(); i != graph_[v].end(); ++i) {      if (!tarjan_dfn[*i]) {        Tarjan(*i);        tarjan_low[v] = min(tarjan_low[v], tarjan_low[*i]);      } else if (tarjan_instack[*i]) {        tarjan_low[v] = min(tarjan_low[v], tarjan_dfn[*i]);      }    }     if (tarjan_dfn[v] == tarjan_low[v]) {      while (tarjan_stack.top() != v) {        tarjan_instack[tarjan_stack.top()] = false;        tarjan_set[tarjan_stack.top()] = v;        tarjan_stack.pop();      }      tarjan_instack[tarjan_stack.top()] = false;      tarjan_set[tarjan_stack.top()] = v;      tarjan_stack.pop();    }  }  static bool compare(const int &a, const int &b) {    return a < b;  }  void unique() {    for (int v = 0; v < vertex_count_; v++) {      graph_[v].sort(compare);      graph_[v].unique();    }  }  void BuildDAG(Graph &new_graph) {    for (int v = 1; v <= vertex_count_; v++) {      for (list<int>::const_iterator i = graph_[v].begin(); i != graph_[v].end(); ++i) {        if (tarjan_set[v] == tarjan_set[*i]) {          continue;        } else {          new_graph.insert_edge(tarjan_set[v], tarjan_set[*i]);        }      }    }    new_graph.unique();  }  int view_degree(int v) {    return degree_[v];  }   void show(int v) {    cout << "Vertex " << v << " : ";    for (list<int>::const_iterator i = graph_[v].begin(); i != graph_[v].end(); ++i) {      cout << *i << " ";    }    cout << endl;  }  void show_all() {    for (int i = 1; i <= vertex_count_; i++) {      show(i);    }  }   int tarjan_count;  int tarjan_set[kMaxN];  int tarjan_dfn[kMaxN];  int tarjan_low[kMaxN];  bool tarjan_instack[kMaxN];  stack<int> tarjan_stack; private:  int vertex_count_;  int degree_[kMaxN];  list<int> graph_[kMaxN];}; int n, p, r;int buy[kMaxN]; int main() {  ios::sync_with_stdio(false);  cin >> n;  Graph graph(n);  Graph graph_dag(n);  cin >> r;  for (int i = 1; i <= r; i++) {    int x, y;    cin >> x >> y;    graph.insert_edge(x, y);  }   graph.TarjanInit();  for (int i = 1; i <= n; i++) {    if (!graph.tarjan_dfn[i]) {      graph.Tarjan(i);    }  }  graph.BuildDAG(graph_dag);   graph_dag.show_all();   return 0;}


求强连通分量的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算法,两者可以类比、组合理解。



原创粉丝点击