如果求一个连通图中以某个顶点为根_图的强连通分量

   一年之计在于春,一天之计在于晨

内容安排:
  1. 图的表示

  2. 图的强连通分量算法:

  • Kosaraju算法

  • Tarjan算法

  • Gabow算法

    3.关于连通性的补充  
图的实现  (1) 邻接矩阵! 开一个二维数组 G。G[i][j]表示边(i,j)的权。如果边(i,j)不存在,就令 G[i][j]=INF(当然,(i,i)不 是一条边,G[i][i]也等于 INF)。邻接矩阵最大的缺点就是内存空间占用太大,内存浪费严重。 (2) 边目录! 设置三个数组 u[M]、v[M]、w[M],分别表示起点、终点和权。一般情况下,从文件中读取的图都是用边目录来表示的。(3) 邻接表(链表)! 用一个列表列出所有与现结点之间有边存在的结点名称。 
struct edge{   int u,v,w;  struct edge *next;} mem[M]; // mem相当于动态内存分配。int size=-1;#define NEW(p) p=&mem[++size]; p->next=NULLstruct edge *adj[N]; // adj[i]代表以i为起点的边。……memset(adj, 0, sizeof(adj));for (int e=0; e{     struct edge *p;     NEW(p);     cin>>(p->u)>>(p->v)>>(p->w);     p->next=adj[p->u];     adj[p->u]=p;}
如果想检查从 a 出发的所有边,那么可以:
for (edge *e=adj[a]; e!=NULL; e=e->next){// e->u是起点,e->v是终点,e->w是权}
(4) 邻接表(静态数组)! 注意,在这个“邻接表”里放置的元素是边的序号,不是点的序号。所以还要和边目录配合使用.
int first[N]; // first[u]表示从u出发的第一条边的序号int u[M],v[M],w[M], next[M]; // next[e]表示编号为e的下一条边的序号memset(first, -1, sizeof(first));for (int e=0; e{  cin>>u[e]>>v[e]>>w[e];  next[e]=first[u[e]]; // 插入一条边  first[u[e]]=e;}
如果想检查从 a 出发的所有边,那么可以:
for (int e=first[a]; e!=-1; e=next[e]){  // u[e]是起点,v[e]是终点,w[e]是权}
(5) 邻接表(STL)!    头文件:这种邻接表也要和边目录配合使用。只需把(4)中的代码换成以下代码:
vector<int> g[N]; // g[u][i]表示从u出发的第i条边的序号int u[M],v[M],w[M]; // 同样要和边目录配合使用for (int e=0; e<m; e++){  cin>>u[e]>>v[e]>>w[e];  g[u].push_back(e);}
如果想检查从 a 出发的所有边,那么可以:
for (int i=0; i{  int &e=g[a][i];  // u[e]是起点,v[e]是终点,w[e]是权}

强连通分量的定义: 有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。 如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

553563a7d95a359b96897c41bb64d921.png

强连通分量(Kosaraju 算法) [邻接矩阵] 该算法可用来计算有向图的强连通分量个数,并收缩强连通分量。 这个算法可以说是最容易理解,最通用的算法,其比较关键的部分是同时应用了原图 G 和反图 G’。操作步骤如下:① 对原图进行 DFS 并将出栈顺序进行逆序,得到的顺序就是拓扑顺序;② 将原图每条边进行反向;③ 按照①中生成顺序再进行 DFS 染色,染成同色的即一个强连通块。该算法具有一个隐藏性质:如果我们把求出来的每个强连通分量收缩成一个点,并且用求出每个强连通分 量的顺序来标记收缩后的结点,那么这个顺序其实就是强连通分量收缩成点后形成的有向无环图的拓扑序列
int dfn[N], top;int color[N], cnt;void dfs1(int k){    color[k] = 1;   for(int i=0; i       if(G[k][i]!=INF && !color[i])            dfs1(i);    dfn[top++] = k; // 出栈顺序逆序,记录第top个出栈的顶点为k}void dfs2(int k){  color[k] = cnt; // 本次DFS染色的点,都属于同一个scc,用num数组做记录  for(int i=0; i     if(G[i][k]!=INF && !color[i]) // 注意,我们在访问原矩阵的对称矩阵        dfs2(i);}int Kosaraju() // 返回强连通分量个数{  top=cnt=0;  memset(color, 0, sizeof(color));  for(int i=0; i// DFS求得拓扑排序  if(!color[i])  dfs1(i); /*  我们本需对原图的边反向,但由于我们使用邻接矩阵储存图, 所以反向的图的邻接矩阵,即原图邻接矩阵的对角线对称矩阵, 所以我们什么都不用做,只需访问对称矩阵即可 */   memset(color, 0, sizeof(color));   for(int i=n-1; i>=0; i--)   if(!color[dfn[i]]){ // 按照拓扑序进行第二次DFS       cnt++;       dfs2(dfn[i]);     }return cnt;}

强连通分量(Tarjan 算法) [邻接表] 该算法的效率要高于 Kosaraju 算法。任何一个强连通分量,必定是对原图的深度优先搜索树的子树(记住这句话)。那么,我们只要确定每个 强连通分量的子树的根,然后根据这些根从树的最低层开始,一个一个的拿出强连通分量。我们维护两个数组,一个是 index,一个是 low。其中 index[i]表示顶点 i 的开始访问时间。low[i]是 此节点能够到达的最前面的位置,初始化为 index[i],维护时 low[i]取它与 low[j]的最小值,其中 j 是与顶点 i 邻接但未删除 的顶点。在一次深搜的回溯过程中,如果发现 low[i]=index[i],那么,当前顶点就是一个强连通分量的根(因 为如果它不是强连通分量的根,那么它一定是属于另一个强连通分量,而且它的根是当前顶点的祖宗,那么存 在包含当前顶点的到其祖宗的回路,可知 low[i]一定被更改为一个比 index[i]更小的值)。拿出强连通分量的方法很简单。如果当前结点为一个强连通分量的根,那么它的强连通分量一定是以该根 为根结点(剩下结点)的子树。在深度优先遍历的时候维护一个堆栈,每次访问一个新结点,就压入堆栈。因为当前结点是这个强连通分量中最先被压入堆栈的,那么在当前结点以后压入堆栈的并且仍在堆栈中的 结点都属于这个强连通分量。 算法实现——对于所有未访问的结点 x,都进行以下操作:① 初始化 index[x]和 low[x];② 对于 x 所有的邻接顶点 v:如果没有访问过,则用同样方法访问 v,同时维护 low[x];如果访问过,但没有删除,就维护 low[x]。③ 如果 index[x]=low[x],那么输出相应的强连通分量
enum _flag { NOTVIS=0, VIS, OVER } flag[N];// NOTVIS、VIS、OVER分别表示顶点没有被访问过、顶点被访问过但未删除、顶点已被删除的状态。int color[N]; // color[i]表示顶点i所属的强连通分量int stack[N], top; // 堆栈,辅助作用int low[N]; // 很关键,与其邻接但未删除顶点的最小访问时间int index[N]; // 顶点访问时间void DFS(int x, int &sig, int &count) // 深搜过程,该算法的主体都在这里{  stack[++top] = x;  flag[x] = VIS;  low[x] = index[x] = ++sig;  for (edge *e=adj[x]; e!=NULL; e=e->next)  {     int &v=e->v;     if (flag[v]==NOTVIS)     {        DFS(v, sig, count);        if (low[v]      }     else if (flag[v]==VIS && index[v]             low[x]=index[v];           // 该部分的index应该是low,但是根据算法的属性,使用index也可以,且时间更少   }   if (low[x]==index[x])   {       count++;       int t;       do      {          t=stack[top--];          color[t] = count;          flag[t] = OVER;        } while (t!=x);    }}int Tarjan(){   int sig, count;   memset(flag, 0, sizeof(flag));   sig=count=top=0;   for (int i=0; i   if (flag[i]==NOTVIS)       DFS(i,sig,count);   return count;}
强连通分量(Gabow 算法) [邻接表] Gabow 算法与 Tarjan 算法的核心思想实质上是相通的,就是利用强连通分量必定是 DFS 的一棵子树这 个重要性质,通过找出这个子树的根来求解强分量。 具体实现是利用一个栈 S 来保存 DFS 遇到的所有树边的另一端顶点,在找出强分量子树的根之后,弹出 S 中的顶点一一进行编号。 与 Tarjan 算法不同的是,Tarjan 算法通过一个 low 数组来维护各个顶点能到达的最小前序编号,而 Gabow 算法通过维护另一个栈来取代 low 数组,将前序编号值更大的顶点都弹出,然后通过栈顶的那个顶点 来判断是否找到强分量子树的根
int pre[N];int color[N];int S[N], P[N]; // 两个栈,S用来保存所有结点,P用来维护路径int top_s, top_p;int cnt, id;void DFS(int x){   int v;   pre[x] = cnt++; // 对前序编号编号   S[++top_s]=x; // 将路径上遇到的树边顶点入栈   P[++top_p]=x;   for (edge *e=adj[x]; e!=NULL; e=e->next)  {    v=e->v;    if (pre[v] == -1) // 如果以前未遇到当前顶点,则对其进行DFS         DFS(v);    else if (color[v] == - 1) // 如果当前顶点不属于强分量,         while (pre[P[top_p]] > pre[v]) // 就将路径栈P中大于当前顶点pre值的顶点都弹出                top_p--;   }    if (P[top_p] == x) // 如果P栈顶元素等于x,则找到强分量的根——x   {       top_p--;       id++;      do     {        v = S[top_s--]; // 把S中的顶点弹出编号        color[v] = id;      } while (v != x);    }}int Gabow(){     top_s=top_p=-1;     memset(pre,-1,sizeof(pre));     memset(color,-1,sizeof(color));     cnt=id=0;     for (int v=0; v         if (pre[v] == -1)              DFS(v);     return id; // 返回id的值,这恰好是强连通分量的个数}
补充: 1. 判断两点是否连通:
  • 方法一:使用 Floyd 算法,时间复杂度 O(n^3)

  • 方法二:从起点出发,使用 DFS 遍历,可以找到与它连通的其他点。时间复杂度 O(n^2)

  • 方法三:(仅用于无向图)使用并查集。时间复杂度 O(an)(a 是一个小常数)。 

2. 统计无向图的强连通分量个数:使用并查集,最后只需统计父亲结点个数。 3.  有向图的传递闭包(Floyd-Warshall 算法) [邻接矩阵] 时间复杂度:O(n^3) Floyd-Warshall 算法会把图上任意两个点的连通性都算出来。
bool f[N][N]; // 如果存在一条从i出发,到j结束的路径,f[i][j]=true。……// 预处理——可以在读图时完成for (int i=0; i<n; i++)   for (int j=0; j<n; j++)       f[i][j] = (G[i][j]!=INF);for (int k=0; k<n; k++)     for (int i=0; i<n; i++)         for (int j=0; j<n; j++)                   f[i][j] = f[i][j] || (f[i][k] && f[k][j]);
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值