总结强连通分量

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了总结强连通分量相关的知识,希望对你有一定的参考价值。

强连通分量总结


概念

来自百度

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

如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)

极大强连通子图?

其实就是使这个环尽量地大


例如
技术分享

对于“极大”的理解,就是在一个局部子图中不能再大。就像是数学中的求一个函数中的极大值和极小值一样,例如求函数f(x)的极大值和极小值,变量x可以有不同的区间,所以在x的不同区间内就会有不同的极大值或极小值。

{1,2,4} {1,3,4}是强连通的;但是,都不是强连通分量

图中的强连通分量为{1,2,3,4} {5} {6}


算法

有Tarjan,Kosaraju,Gabow等

Tarjan

这大概是我唯一记得的了

简单原理

tarjan算法基于dfs算法,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说强连通分量一定是有向图的某个深度优先搜索生成树。

用low值记录点u所在强连通子图对应的搜索子树的根节点的dfs值。该子树中的元素在栈中一定是相邻的,且根节点在栈中一定位于所有子树元素的最下方。

强连通分量是由若干个环组成,所以当有环形成时,我们将这一条路径的low值统一,即这条路径上的所有点属于同一个强连通分量。

如果遍历完整个搜索树后某个点的dfn值等于low值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。

 

规则
  1. 数组初始化:定义两个数组dfn,dfn[i]代表第一次到达i时的时间,当首次搜索到点u时,low[i]初始化为dfn[i]

  2. 堆栈:每遍历到一个未被标记的点u,将它入栈。

  3. 再从当前点u开始遍历,当从v回溯到u时,如果点v不在栈中,那么low[u] = min{low[u],low[v]};如果点v在栈中,那么low[u] = min{low[u],dfn[v]}

  4. 每当搜索到一个点i并经过以上步骤后,如果其low[i]=dfn[i],则将它以及在它之上的元素弹出栈。这些出栈元素组成一个强连通分量。

  5. 继续搜索(因为有向图可能有多个连通子图组成,而这些子图没有交集),直到所有点被遍历。

 

演示

引用网上一篇博客的算法演示http://kmplayer.iteye.com/blog/604532

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

我们遍历到节点6,发现已经搜到底,不能再遍历,判断DFN[6]=LOW[6],弹出。6也是一个强连通分量

技术分享

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

回溯到5,low[5] = min{low[5],low[6]},发现已无处可去,判断DFN[5]=LOW[5],将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)。

求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。

 

代码

#include<algorithm>
#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int N = 105;
int n, cnt, tim, dcnt, hd[N], dfn[N], low[N], belong[N], ru[N], chu[N];      //tim记时间,dcnt为强连通分量的数量,belong[i]=k表示点i在第k个强连通分量中
bool instk[N];
stack<int>stk;
struct edge  {
  int to, nxt;
} v[N * N];
void addedge(int x, int y)  {
  ++cnt;
  v[cnt].to = y;
  v[cnt].nxt = hd[x];
  hd[x] = cnt;
}
void tarjan(int u)  {
  dfn[u] = low[u] = ++tim;
  instk[u] = 1;
  stk.push(u);
  for (int i = hd[u]; i; i = v[i].nxt)
    if (dfn[v[i].to] == 0)  {
      tarjan(v[i].to);
      low[u] = min(low[u], low[v[i].to]);
    }
    else if (instk[v[i].to]) low[u] = min(low[u], dfn[v[i].to]);
  if (low[u] == dfn[u])  {
    ++dcnt;
    while (1)  {
      int t = stk.top();
      stk.pop();
      instk[t] = 0;
      belong[t] = dcnt;
      if (t == u)  break;
    }
  }
}
int main()  {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++)  {
    int x;
    while (scanf("%d", &x) && x) addedge(i, x);
  }
  for (int i = 1; i <= n; i++)  if (dfn[i] == 0)  tarjan(i);
  printf("%d",dcnt);
  return 0;

 

举例

校园网
传送门

题目

一些学校连入一个电脑网络。那些学校已订立了协议:每个学校都会给其它的一些学校分发软件(称作“接受学校”)。注意即使 B 在 A 学校的分发列表中, A 也不一定在 B 学校的列表中。

你要写一个程序计算,根据协议,为了让网络中所有的学校都用上新软件,必须接受新软件副本的最少学校数目(子任务 A)。更进一步,我们想要确定通过给任意一个学校发送新软件,这个软件就会分发到网络中的所有学校。为了完成这个任务,我们可能必须扩展接收学校列表,使其加入新成员。计算最少需要增加几个扩展,使得不论我们给哪个学校发送新软件,它都会到达其余所有的学校(子任务 B)。一个扩展就是在一个学校的接收学校列表中引入一个新成员。

 

输入格式:

输入文件的第一行包括一个整数 N:网络中的学校数目(2 <= N <= 100)。学校用前 N 个正整数标识。

接下来 N 行中每行都表示一个接收学校列表(分发列表)。第 i+1 行包括学校 i 的接收学校的标识符。每个列表用 0 结束。空列表只用一个 0 表示。

 

输出格式:

你的程序应该在输出文件中输出两行。

第一行应该包括一个正整数:子任务 A 的解。

第二行应该包括子任务 B 的解。

 

输入样例
5
2 4 3 0
4 5 0
0
0
1 0

 

输出样例
1
2

 

思路

对于子任务A 用Tarjan缩点后,入度为0的点即为子任务A的解

对于子任务B 求入度为0的点个数与出度为0的点个数的最大值 需要添多少条边才能形成使所有点都可以被某个点到达 所以对于每个出度为0的点 需要拓展一条出边 对于每个入度为0的点 需要拓展一条入边 为了减少拓展边数 可以将所有出度为0的点拓展出边到某个入度为0的点的上 最后答案即为 max(入度为0点数,出度为0点数)

 

代码
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int N = 105;
int n, cnt, tim, dcnt, cnt1, cnt2, ans1, ans2, hd[N], dfn[N], low[N], belong[N], ru[N], chu[N];
bool instk[N];
stack<int>stk;
struct edge  {
  int to, nxt;
} v[N * N];
void addedge(int x, int y)  {
  ++cnt;
  v[cnt].to = y;
  v[cnt].nxt = hd[x];
  hd[x] = cnt;
}
void tarjan(int u)  {
  dfn[u] = low[u] = ++tim;
  instk[u] = 1;
  stk.push(u);
  for (int i = hd[u]; i; i = v[i].nxt)
    if (dfn[v[i].to] == 0)  {
      tarjan(v[i].to);
      low[u] = min(low[u], low[v[i].to]);
    }
    else if (instk[v[i].to]) low[u] = min(low[u], dfn[v[i].to]);
  if (low[u] == dfn[u])  {
    ++dcnt;
    while (1)  {
      int t = stk.top();
      stk.pop();
      instk[t] = 0;
      belong[t] = dcnt;
      if (t == u)  break;
    }
  }
}
int main()  {
  freopen("schlnet.in", "r", stdin);
  freopen("schlnet.out", "w", stdout);
  scanf("%d", &n);
  for (int i = 1; i <= n; i++)  {
    int x;
    while (scanf("%d", &x) && x) addedge(i, x);
  }
  for (int i = 1; i <= n; i++)  if (dfn[i] == 0)  tarjan(i);
  for (int i = 1; i <= n; i++)
    for (int j = hd[i]; j; j = v[j].nxt)
      if (belong[i] != belong[v[j].to]) ru[belong[v[j].to]]++, chu[belong[i]]++;
  for (int i = 1; i <= dcnt; i++)  {
    if (ru[i] == 0) ans1++, cnt1++;
    if (chu[i] == 0)  cnt2++;
  }
  ans2 = max(cnt1, cnt2);
  if (dcnt == 1) ans2 = 0;
  printf("%d\\n%d\\n", ans1, ans2);
  return 0;
}

 


Kosaraju和Gabow

等我想起来,再说吧


总结

还早着呢

?

以上是关于总结强连通分量的主要内容,如果未能解决你的问题,请参考以下文章

双连通分量和强连通分量的总结

tarjan算法(强连通分量 + 强连通分量缩点 + 桥 + 割点 + LCA)

强连通分量(tarjan求强连通分量)

强连通分量——tarjan算法

HDU 1269 迷宫城堡 tarjan算法求强连通分量

Kosaraju算法——强连通分量