数据结构的连通性问题

来源:互联网 发布:软件过程管理期末试卷 编辑:程序博客网 时间:2024/04/20 18:45

连通性问题,这可真是tarjan的天下啊,不过这篇文章并没有打算扯到tarjan的起源模型强连通分量,主要还是说说自己对其它连通性问题的思考,所以,如果你还不会tarjan算法的话,嗯,点这里:byvoid的tarjan算法讲解  膜拜一下神牛。

  当然了,关于连通性问题这里还有:byvoid的连通性问题讲解  再次膜拜。
  这篇文章是自己将三篇研究日记汇总而成的,所以中间有一部分属于含有错误的,标题已经进行了警示,大家也可以找找看为什么不对,文章后面进行了订正与说明,好的,进入正题。


基本概念:


        1、连通:两个点之间存在若干条边将其连接,称其连通

        2、强连通:有向图中的两点可以互达(A→B 并且 B→A),称其强连通
        3、弱连通:有向图中的两点可以到达(A→B 或者 B→A),称其弱连通
        4、连通图:图G中任意两点都连通,则G为连通图
        5、强连通图:有向图G中任意两点都强连通,则G为强连通图
        6、弱连通图:有向图G中任意两点都弱连通,则G为弱连通图
        7、强连通分量:非强连通图的极大强连通子图,称为强连通分量(极大指不能再大,与最大的意义不同)
        8、点连通度:使无向图G不连通的最少删点数量为其点连通度
        9、边连通度:使无向图G不连通的最少删边数量为其边连通度
        10、点双连通图:点连通度大于1的无向图
        11、边双连通图:边连通度大于1的无向图
        12、双连通图:点连通度和边连通度均大于1的无向图
        13、点双连通分量:非点双连通图的极大点双连通子图
        14、边双连通分量:非边双连通图的极大边双连通子图
        15、双连通分量:非双连通图的极大双连通子图
        16、割点:点连通度为1的无向图中,被删除后将导致原图不连通的点
        17、桥:边连通度为1的无向图中,被删除后将导致原图不连通的边
        18、返祖边:在DFS中连接当前点与未访问完毕的点之间的边

        19、横叉边:在DFS中连接当前点与已访问完毕的点之间的边

        20、后向边:就是在DFS中,子孙指向祖先的边。


双连通分量:

    

    双连通分量有两种:点双连通分量、边双连通分量。那双连通分量又是什么?到底是点的还是边的?这样不清楚的表述屡见不鲜,参考了众多人的博客后,关于双连通分量的定义,还是确定不下来,主要有以下几种说法:

  1、指点双连通,与块同义
  2、指边双连通
  3、有时指点双连通,有时指边双连通
  4、满足点双连通或者边双连通
  5、同时满足点双连通与边双连通
  关于双连通的定义,众说纷纭,我觉得还是不要盲目相信任何人,毕竟说清楚是点双连通还是边双连通并没有碍多少事,那么以后就说清楚为好,免得出现歧义。


关系:


        在一个点数大于2的图中,有桥就一定有割点,但是有割点不一定有桥。也就是说,不是点连通图的一定也不是边连通图,但是不是边联通图的不一定不是点连通图。


算法:


  当然还是tarjan了。
  用dfn表示时间戳,用low表示简单环内的最小时间戳
  强连通分量:当dfn[u] == low[u]
  :当dfn[u] < low[v]
  割点:当dfn[u] <= low[v] 
  这又从算法的角度印证了上面的结论:有桥则一定有割点,但是有割点不一定有桥,因为该点可以是环内搜索树的根节点,当没有该点的时候,环上各点将与该点的搜索树祖先节点不连通,但是若消去环上一边,环上各点与该点依然连通,这意味着它们与该点的祖先节点依然连通。


横叉边:


  横叉边是一个定义在有向图搜索树中的概念,对于无向图它是没有任何意义的。有向图出现横叉边的原因是u→v不可行,然后u已经退栈成功,然而v→u可行,所以会访问到已经退栈的节点,这样的边称之为横叉边,然而在无向图中,这样的情况是不可能出现的,如果u→v是可行的,那么v→u也是可行的,因为无向图中的边是没有方向的,那么,在求无向图相关的桥、割点、点双连通分量、边双连通分量的时候,就不需要开一个布尔数组来记录该点是否访问完毕,即是否还在栈中,并且,也不需要开布尔数组来记录该点是否已经访问过,因为访问过的点dfn <> 0,据此可知,在无向图的连通求解中,可以不开任何布尔数组,切记切记!!

有向图强连通分量 Tarjan算法

[有向图强连通分量]

在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。

下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。

wps_clip_image-24103

[Tarjan算法]

Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。

 

定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。

 

算法伪代码如下

tarjan(u) 
{

    DFN[u]=Low[u]=++Index     // 为节点u设定次序编号和Low初值

    Stack.push(u)                     // 将节点u压入栈中

    for each (u, v) in E               // 枚举每一条边

          if (v is not visted)          // 如果节点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是强连通分量的根

       repeat

           v = S.pop                  // 将v退栈,为该强连通分量中一个顶点

           print v

      until (u== v)

}

 

接下来是对算法流程的演示。

从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

wps_clip_image-16442

返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。

wps_clip_image-24939

返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

wps_clip_image-17734

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

wps_clip_image-10846

至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。

可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。

tarjan算法的简单证明:

         首先,这边再重复一下什么是后向边:就是在深度优先搜索中,子孙指向祖先的边。在一棵深度优先搜索树中,对于结点v, 和其父亲结点u而言,u,v 属于同一个强连通分支的充分必要条件是  以v为根的子树中,有一条后向边指向u或者u的祖先。

1 、必要性。  
           如果 u,v属于同一个强连通分支则必定存在一条 u到 v的路径和一条v到u的路径。合并两条则有 u->v->v1->v2->..vn->u, 若顶点v1到vn都是v 的子孙,则有 vn->u这样一条后向边。
          如果v1到vn 不全是vn的子孙,则必定有一个是u的祖先,我们不妨设vi为u的祖先,则有一条后向边 V[i-1] ->v[i]。

2.、充分性。    我们设 u1->u2->u3..->un->u->v->v1->v2..->vn,我们假设后向边vn指向ui则有这样一个环:u[i]->u[i+1]...->u->v->v1->v2..->v[n-1]->v[n]->u[i],易知,有一条u->v的路径,同时有v->u的路径。固u,v属于同一连通分支。

        在算法开始的时候,我们把i圧入栈中。根据low[i] 和 dfn[i]的定义我们知道,
        如果low[i] < dfn[i] 则以i为顶点的子树中,有指向祖先的后向边,则说明i和i的父亲为在同一连通分支,也就是说留在栈中的元素都是和父结点在同一连通分支的
         如果low[i] == dfn[i],则 i为顶点的子树中没有后向边,那么由于  留在栈中的元素都是和父结点在同一连通分支的,我们可以知道,从栈顶到元素i构成了一个连通分支。显然,low[i]不可能小于dfn[i]。
3、Tarjan算法基于定理:在任何深度优先搜索中,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说,强连通分量一定是有向图的某个深搜树子树。

针对tarjan的操作规则来讲解这个算法

      其实,tarjan算法的基础是DFS。我们准备两个数组Low和Dfn。Low数组是一个标记数组,记录该点所在的强连通子图所在搜索子树的根节点的Dfn值(很绕嘴,往下看你就会明白),Dfn数组记录搜索到该点的时间,也就是第几个搜索这个点的。根据以下几条规则,经过搜索遍历该图(无需回溯)和对栈的操作,我们就可以得到该有向图的强连通分量。

 

  1. 数组的初始化:当首次搜索到点p时,Dfn与Low数组的值都为到该点的时间。
  2. 堆栈:每搜索到一个点,将它压入栈顶。
  3. 当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’不在栈中,p的low值为两点的low值中较小的一个。
  4. 当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’在栈中,p的low值为p的low值和p’的dfn值中较小的一个。
  5. 每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的low值等于dfn值,则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。
  6. 继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。

      由于每个顶点只访问过一次,每条边也只访问过一次,我们就可以在O(n+m)的时间内求出有向图的强连通分量。但是,这么做的原因是什么呢?

 

      Tarjan算法的操作原理如下:

  1. Tarjan算法基于定理:在任何深度优先搜索中,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说,强连通分量一定是有向图的某个深搜树子树。
  2. 可以证明,当一个点既是强连通子图Ⅰ中的点,又是强连通子图Ⅱ中的点,则它是强连通子图Ⅰ∪Ⅱ中的点。
  3. 这样,我们用low值记录该点所在强连通子图对应的搜索子树的根节点的Dfn值。注意,该子树中的元素在栈中一定是相邻的,且根节点在栈中一定位于所有子树元素的最下方。
  4. 强连通分量是由若干个环组成的。所以,当有环形成时(也就是搜索的下一个点已在栈中),我们将这一条路径的low值统一,即这条路径上的点属于同一个强连通分量。
  5. 如果遍历完整个搜索树后某个点的dfn值等于low值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。

求有向图的强连通分量还有一个强有力的算法,为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表示崇高的敬意。

#include "cstdlib" #include "cctype" #include "cstring" #include "cstdio" #include "cmath" #include "algorithm" #include "vector" #include "string" #include "iostream" #include "sstream" #include "set" #include "queue" #include "stack" #include "fstream" //#include "strstream" using namespace std;#define  M 2000              //题目中可能的最大点数       int STACK[M],top=0;          //Tarjan 算法中的栈 bool InStack[M];             //检查是否在栈中 int DFN[M];                  //深度优先搜索访问次序 int Low[M];                  //能追溯到的最早的次序 int ComponetNumber=0;        //有向图强连通分量个数 int Index=0;                 //索引号 vector <int> Edge[M];        //邻接表表示 vector <int> Component[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])     {         cout<<"TT    "<<i<<"   "<<Low[i]<<endl;         ComponetNumber++;         do         {             j=STACK[top--];             InStack[j]=false;             Component[ComponetNumber].push_back(j);         }         while (j!=i);     } }void solve(int 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);    } /* 此算法正常工作的基础是图是0-indexed的。 */ int main() {     Edge[0].push_back(2);Edge[0].push_back(1);     Edge[1].push_back(3);     Edge[2].push_back(4);Edge[2].push_back(3);     Edge[3].push_back(0);Edge[3].push_back(5);     Edge[4].push_back(5);     int  N=6;     solve(N);     cout<<"ComponetNumber is "<<ComponetNumber<<endl;     for(int i=0;i<N;i++)         cout<<Low[i]<<" ";     cout<<endl;     for(int i=0;i<N;i++)     {         for(int j=0;j<Component[i].size();j++)             cout<<Component[i][j];         cout<<endl;     }     return 0; }
自创版:
// virtualDestruction.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>#include <algorithm>#include <vector>#include <stack>//#include <>using namespace std;#define M 2000vector<int> Edge[M];//邻接表vector<int> res[M];//强连通分量int componentNum=0;//强连通分量个数int dfn[M];//每个点的遍历次序int index=0;//次序索引int low[M];//该点所在强连通分量所在搜索树的根节点次序号stack<int> seq;//遍历的点bool inStack[M];void tarjan(int i){dfn[i]=low[i]=index++;seq.push(i);inStack[i]=true;for (int j=0;j<Edge[i].size();j++){if (dfn[Edge[i][j]]==-1){tarjan(Edge[i][j]);low[i]=min(low[i],low[Edge[i][j]]);}else if (inStack[Edge[i][j]])low[i]=min(low[i],low[Edge[i][j]]);//dfn->low} if (dfn[i]==low[i]) {int k=i;componentNum++;do{  inStack[i]=false;  k=seq.top();  res[componentNum-1].push_back(k);  seq.pop();}while(i!=k); }}int _tmain(int argc, _TCHAR* argv[]){int N=6;Edge[0].push_back(2);Edge[0].push_back(1);     Edge[1].push_back(3);     Edge[2].push_back(4);Edge[2].push_back(3);     Edge[3].push_back(0);Edge[3].push_back(5);     Edge[4].push_back(5); memset(dfn,-1,sizeof(dfn));memset(low,-1,sizeof(low));memset(inStack,0,sizeof(inStack));for(int i=0;i<N;i++)if (dfn[i]==-1)tarjan(i);cout<<"Num: "<<componentNum<<endl;for (int i=0;i<componentNum;i++){for (int j=0;j<res[i].size();j++)cout<<res[i][j]<<" ";cout<<endl;}return 0;}

  下面是算法的一个模板:

#include <stdio.h>  #include <string.h>  #include <stdlib.h>    //从顶点0开始  // 要用的话要初始化:调用Adj.initial 和 tarjan.initial  //要解决问题用调用tarjan.solve  //对tarjan.initial要传入的参数是图边集Adj,和顶点个数n    const int maxn = 11000;  //顶点的规模  const int maxm = 210000;  //边的规模,如果是无向图要记得乘以2    const int GRAY = 0;  const int WHITE =-1;  const int BLACK = 1;    typedef struct Edge{      int s;      int e;      int next;  }Edge;    typedef struct Adj{      int edge_sum;      int head[maxn];      Edge edge[maxm];        void initial(){           edge_sum = 0;          memset(head,-1,sizeof(head));      }        void add_edge(int a, int b){          edge[edge_sum].s = a;          edge[edge_sum].e = b;          edge[edge_sum].next = head[a];          head[a] = edge_sum++;      }  }Adj;    typedef struct Tanjan{      int n;      int *head;      Adj *adj;      Edge *edge;        int cnt;      int top;      int cur;        int dfn[maxn];      int low[maxn];      int color[maxn];      int stack[maxn];      int belong[maxn];        void initial(Adj *_adj,int _n){          n = _n;          adj = _adj;          head = (*adj).head;          edge = (*adj).edge;      }        void solve(){          memset(dfn,-1,sizeof(dfn));          memset(color,WHITE,sizeof(color));            top = cnt = cur = 0;          for(int i = 0; i < n; i++)              if(color[i] == WHITE)//找到一个白色的顶点,就开始处理                  tarjan(i);      }        inline int min(int a, int b){          if(a < b) return a;          else return b;      }        void tarjan(int i){          int j = head[i];            color[i] = GRAY;//标记为灰色          stack[top++] = i;//把结点圧入栈顶            dfn[i] = low[i] = ++cur;//给结点一个时间戳,并给Low初始化            while(j != -1){              int u = edge[j].e;              if        (dfn[u] == WHITE){                  tarjan(u);                  low[i] = min(low[i],low[u]);              //更新low               }else  if (color[u] == GRAY)                  low[i] = min(low[i],dfn[u]);              //一条后向边              j = edge[j].next;          }            color[i] = BLACK;          if(low[i] == dfn[i]){              do{                  j = stack[--top];                  belong[j] = cnt;              }while(i != j);              ++cnt;            }      }  }Tarjan;    Adj adj;  Tarjan tj;


参考网址:

http://lib.csdn.net/article/datastructure/10310

http://www.cppblog.com/sosi/archive/2010/09/26/127797.aspx

http://blog.csdn.net/nothi/article/details/7739741

http://blog.csdn.net/e6894853/article/details/7898185

http://blog.csdn.net/xinghongduo/article/details/6195337

0 0
原创粉丝点击