强连通分量
简介
在阅读下列内容之前,请务必了解图论基础部分。
强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。
强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。
这里想要介绍的是如何来求强连通分量。
Tarjan 算法
Robert E. Tarjan (1948~) 美国人。
Tarjan 发明了很多算法结构。光 Tarjan 算法就有很多,比如求各种联通分量的 Tarjan 算法,求 LCA(Lowest Common Ancestor,最近公共祖先)的 Tarjan 算法。并查集、Splay、Toptree 也是 Tarjan 发明的。
我们这里要介绍的是在有向图中求强连通分量的 Tarjan 算法。
另外,Tarjan 的名字 j
不发音,中文译为塔扬。
DFS 生成树
在介绍该算法之前,先来了解 DFS 生成树 ,我们以下面的有向图为例:
有向图的 DFS 生成树主要有 4 种边(不一定全部出现):
- 树边(tree edge):绿色边,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
- 反祖边(back edge):黄色边,也被叫做回边,即指向祖先结点的边。
- 横叉边(cross edge):红色边,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先时形成的。
- 前向边(forward edge):蓝色边,它是在搜索的时候遇到子树中的结点的时候形成的。
我们考虑 DFS 生成树与强连通分量之间的关系。
如果结点u是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以u为根的子树中。u被称为这个强连通分量的根。
反证法:假设有个结点v在该强连通分量中但是不在以u为根的子树中,那么u到v的路径中肯定有一条离开子树的边。但是这样的边只可能是横叉边或者反祖边,然而这两条边都要求指向的结点已经被访问过了,这就和u是第一个访问的结点矛盾了。得证。
Tarjan 算法求强连通分量
在 Tarjan 算法中为每个结点u维护了以下几个变量:
- DFN[u]:深度优先搜索遍历时结点 被搜索的次序。
- LOW[u]:设以u为根的子树为Subtree(u) 。 LOW[u]定义为以下结点的 的最小值:Subtree(u)中的结点;从Subtree(u)通过一条不在搜索树上的边能到达的结点。
一个结点的子树内结点的 DFN 都大于该结点的 DFN。
从根开始的一条路径上的 DFN 严格递增,LOW 严格非降。
按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索。在搜索过程中,对于结点 和与其相邻的结点 (v 不是 u 的父节点)考虑 3 种情况:
- v未被访问:继续对v进行深度搜索。在回溯过程中,用LOW[v]更新LOW[u]。因为存在从u到v的直接路径,所以v能够回溯到的已经在栈中的结点,u也一定能够回溯到。
- v被访问过,已经在栈中:即已经被访问过,根据LOW值的定义(能够回溯到的最早的已经在栈中的结点),则用DFN[v]更新LOW[u]。
- v被访问过,已不在在栈中:说明v已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
将上述算法写成伪代码:
1 TARJAN_SEARCH(int u) 2 vis[u]=true 3 low[u]=dfn[u]=++dfncnt 4 push u to the stack 5 for each (u,v) then do 6 if v hasn‘t been search then 7 TARJAN_SEARCH(v) // 搜索 8 low[u]=min(low[u],low[v])// 回溯 9 else if v has been in the stack then 10 low[u]=min(low[u],dfn[v])
对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个DFN[u]=LOW[u] 。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 DFN 值和 LOW 值最小,不会被该连通分量中的其他结点所影响。
因此,在回溯的过程中,判定DFN[u]=LOW[u]的条件是否成立,如果成立,则栈中从u后面的结点构成一个 SCC。
实现
1 int dfn[N], low[N], dfncnt, s[N], tp; 2 int scc[N], sc; // 结点 i 所在 scc 的编号 3 int sz[N]; // 强连通 i 的大小 4 void tarjan(int u) { 5 low[u] = dfn[u] = ++dfncnt, s[++tp] = u; 6 for (int i = h[u]; i; i = e[i].nex) { 7 const int &v = e[i].t; 8 if (!dfn[v]) 9 tarjan(v), low[u] = min(low[u], low[v]); 10 else if (!scc[v]) 11 low[u] = min(low[u], dfn[v]); 12 } 13 if (dfn[u] == low[u]) { 14 ++sc; 15 while (s[tp] != u) scc[s[tp]] = sc, sz[sc]++, --tp; 16 scc[s[tp]] = sc, sz[sc]++, --tp; 17 } 18 }
时间复杂度O(n+m) 。
Kosaraju 算法
Kosaraju 算法依靠两次简单的 DFS 实现。
第一次 DFS,选取任意顶点作为起点,遍历所有为访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。
第二次 DFS,对于反向后的图,以标号最大的顶点作为起点开始 DFS。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。
两次 DFS 结束后,强连通分量就找出来了,Kosaraju 算法的时间复杂度为O(n+m)。
实现
1 // g 是原图,g2 是反图 2 3 void dfs1(int u) { 4 vis[u] = true; 5 for (int v : g[u]) 6 if (!vis[v]) dfs1(v); 7 s.push_back(v); 8 } 9 10 void dfs2(int u) { 11 color[u] = sccCnt; 12 for (int v : g2[u]) 13 if (!color[v]) dfs2(v); 14 } 15 16 void kosaraju() { 17 sccCnt = 0; 18 for (int i = 1; i <= n; ++i) 19 if (!vis[i]) dfs1(i); 20 for (int i = n; i >= 1; --i) 21 if (!color[s[i]]) { 22 ++sccCnt; 23 dfs2(s[i]) 24 } 25 }
Garbow 算法
应用
我们可以将一张图的每个强连通分量都缩成一个点。
然后这张图会变成一个 DAG(为什么?)。
DAG 好啊,能拓扑排序了就能做很多事情了。
举个简单的例子,求一条路径,可以经过重复结点,要求经过的不同结点数量最多。
推荐题目
接下来我们讨论一下Tarjan算法能够干一些什么:
既然我们知道,Tarjan算法相当于在一个有向图中找有向环,那么我们Tarjan算法最直接的能力就是缩点辣!缩点基于一种染色实现,我们在Dfs的过程中,尝试把属于同一个强连通分量的点都染成一个颜色,那么同一个颜色的点,就相当于一个点。比如刚才的实例图中缩点之后就可以变成这样:
将一个有向带环图变成了一个有向无环图(DAG图)。很多算法要基于有向无环图才能进行的算法就需要使用Tarjan算法实现染色缩点,建一个DAG图然后再进行算法处理。在这种场合,Tarjan算法就有了很大的用武之地辣!
那么这个时候 ,我们再引入一个数组color【i】表示节点i的颜色,再引入一个数组stack【】实现一个栈,然后在Dfs过程中每一次遇到点都将点入栈,在每一次遇到关键点的时候将栈内元素弹出,一直弹到栈顶元素是关键点的时候为止,对这些弹出来的元素进行染色即可。
1 void Tarjan(int u)//此代码仅供参考 2 { 3 vis[u]=1; 4 low[u]=dfn[u]=cnt++; 5 stack[++tt]=u; 6 for(int i=0;i<mp[u].size();i++) 7 { 8 int v=mp[u][i]; 9 if(vis[v]==0)Tarjan(v); 10 if(vis[v]==1)low[u]=min(low[u],low[v]); 11 } 12 if(dfn[u]==low[u]) 13 { 14 sig++; 15 do 16 { 17 low[stack[tt]]=sig; 18 color[stack[tt]]=sig; 19 vis[stack[tt]]=-1; 20 } 21 while(stack[tt--]!=u); 22 } 23 }
原文:https://blog.csdn.net/justlovetao/article/details/6673602
有向图强连通分量的Tarjan算法 [有向图强连通分量]
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
下图中,子图{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的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出,
1 Low(u)=Min 2 { 3 DFN(u), 4 Low(v),(u,v)为树枝边,u为v的父节点 5 DFN(v),(u,v)为指向栈中节点的后向边(非横叉边) 6 }
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。
算法伪代码如下
1 tarjan(u) 2 { 3 DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值 4 Stack.push(u) // 将节点u压入栈中 5 for each (u, v) in E // 枚举每一条边 6 if (v is not visted) // 如果节点v未被访问过 7 tarjan(v) // 继续向下找 8 Low[u] = min(Low[u], Low[v]) 9 else if (v in S) // 如果节点v还在栈内 10 Low[u] = min(Low[u], DFN[v]) 11 if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根 12 repeat 13 v = S.pop // 将v退栈,为该强连通分量中一个顶点 14 print v 15 until (u== v) 16 }
接下来是对算法流程的演示。
从节点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已经出栈,(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算法,两者可以类比、组合理解。
求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。
附:tarjan算法的C++程序
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 using namespace std; 5 #define N 100 6 #define M 100 7 struct Edge 8 { 9 int v; 10 int next; 11 }; 12 Edge edge[M];//边的集合 13 14 int node[N];//顶点集合 15 int instack[N];//标记是否在stack中 16 int stack[N]; 17 int Belong[N];//各顶点属于哪个强连通分量 18 int DFN[N];//节点u搜索的序号(时间戳) 19 int LOW[N];//u或u的子树能够追溯到的最早的栈中节点的序号(时间戳) 20 int n, m;//n:点的个数;m:边的条数 21 int cnt_edge;//边的计数器 22 int Index;//序号(时间戳) 23 int top; 24 int Bcnt;//有多少个强连通分量 25 26 void add_edge(int u, int v)//邻接表存储 27 { 28 edge[cnt_edge].next = node[u]; 29 edge[cnt_edge].v = v; 30 node[u] = cnt_edge++; 31 } 32 void tarjan(int u) 33 { 34 int i,j; 35 int v; 36 DFN[u]=LOW[u]=++Index; 37 instack[u]=true; 38 stack[++top]=u; 39 for (i = node[u]; i != -1; i = edge[i].next) 40 { 41 v=edge[i].v; 42 if (!DFN[v])//如果点v没被访问 43 { 44 tarjan(v); 45 if (LOW[v]<LOW[u]) 46 LOW[u]=LOW[v]; 47 } 48 else//如果点v已经被访问过 49 if (instack[v] && DFN[v]<LOW[u]) 50 LOW[u]=DFN[v]; 51 } 52 if (DFN[u]==LOW[u]) 53 { 54 Bcnt++; 55 do 56 { 57 j=stack[top--]; 58 instack[j]=false; 59 Belong[j]=Bcnt; 60 } 61 while (j!=u); 62 } 63 } 64 void solve() 65 { 66 int i; 67 top=Bcnt=Index=0; 68 memset(DFN,0,sizeof(DFN)); 69 memset(LOW,0,sizeof(LOW)); 70 for (i=1;i<=n;i++) 71 if (!DFN[i]) 72 tarjan(i); 73 } 74 int main() 75 { 76 freopen("in.txt","r",stdin); 77 int i,j,k; 78 cnt_edge=0; 79 memset(node,-1,sizeof(node)); 80 scanf("%d%d",&n,&m); 81 for(i=1;i<=m;i++) 82 { 83 scanf("%d%d",&j,&k); 84 add_edge(j,k); 85 } 86 solve(); 87 for(i=1;i<=n;i++) 88 printf("%d ",Belong[i]); 89 } 90
我自己根据模板写的适合我用的(可以忽略)
1 #include <stdio.h> 2 #include <string.h> 3 #include <algorithm> 4 #include <stack> 5 using namespace std; 6 #define N 100 7 #define M 100 8 9 struct Edge{ 10 int v; 11 int next; 12 }Edge[M];//边的集合 13 14 int node[N];//顶点集合 15 int instack[N];//标记是否在stack中 16 int Belong[N];//各顶点属于哪个强连通分量 17 int DFN[N];//节点u搜索的序号(时间戳) 18 int LOW[N];//u或u的子树能够追溯到的最早的栈中节点的序号(时间戳) 19 int n,m;//n:点的个数;m:边的条数 20 int cnt_edge;//边的计数器 21 int Index;//序号(时间戳) 22 int Bcnt; //有多少个强连通分量 23 stack<int> sk; 24 25 void add_edge(int u,int v)//邻接表存储 26 { 27 Edge[cnt_edge].next=node[u]; 28 Edge[cnt_edge].v=v; 29 node[u]=cnt_edge++; 30 } 31 32 void tarjan(int u) 33 { 34 DFN[u]=LOW[u]=++Index; 35 instack[u]=1; 36 sk.push(u); 37 for(int i=node[u];i!=-1;i=Edge[i].next) 38 { 39 int v=Edge[i].v; 40 if(!DFN[v])//如果点v没被访问 41 { 42 tarjan(v); 43 LOW[u]=min(LOW[u],LOW[v]); 44 } 45 else //如果点v已经被访问过 46 { 47 if(instack[v]&&DFN[v]<LOW[u]) 48 LOW[u]=DFN[v]; 49 } 50 } 51 if(DFN[u]==LOW[u]) 52 { 53 Bcnt++; 54 int t; 55 do{ 56 t=sk.top(); 57 sk.pop(); 58 instack[t]=0; 59 Belong[t]=Bcnt; 60 }while(t!=u); 61 } 62 } 63 64 int main() 65 { 66 freopen("sample.txt","r",stdin); 67 memset(node,-1,sizeof(node)); 68 scanf("%d %d",&n,&m); 69 for(int i=1;i<=m;i++) 70 { 71 int a,b; 72 scanf("%d %d",&a,&b); 73 add_edge(a,b); 74 } 75 for(int i=1;i<=n;i++) 76 { 77 if(!DFN[i]) 78 { 79 tarjan(i); 80 } 81 } 82 for(int i=1;i<=n;i++) 83 { 84 printf("%d ",Belong[i]); 85 } 86 return 0; 87 }
Network of Schools
http://poj.org/problem?id=1236
Description
A number of schools are connected to a computer network. Agreements have been developed among those schools: each school maintains a list of schools to which it distributes software (the “receiving schools”). Note that if B is in the distribution list of school A, then A does not necessarily appear in the list of school B
You are to write a program that computes the minimal number of schools that must receive a copy of the new software in order for the software to reach all schools in the network according to the agreement (Subtask A). As a further task, we want to ensure that by sending the copy of new software to an arbitrary school, this software will reach all schools in the network. To achieve this goal we may have to extend the lists of receivers by new members. Compute the minimal number of extensions that have to be made so that whatever school we send the new software to, it will reach all other schools (Subtask B). One extension means introducing one new member into the list of receivers of one school.
Input
The first line contains an integer N: the number of schools in the network (2 <= N <= 100). The schools are identified by the first N positive integers. Each of the next N lines describes a list of receivers. The line i+1 contains the identifiers of the receivers of school i. Each list ends with a 0. An empty list contains a 0 alone in the line.
Output
Your program should write two lines to the standard output. The first line should contain one positive integer: the solution of subtask A. The second line should contain the solution of subtask B.
Sample Input
5 2 4 3 0 4 5 0 0 0 1 0
Sample Output
1 2
题目大意:给你一个有向图,求出最少选几个点,可以使整个图都是它们及它们的子节点。再求出最少加几条边可使原图变为强连通图。
题解:此题需要稍稍思考一下。求强连通分量并进行缩点后,设入度为0的点有a个,出度为零的点有b个。则ans1=a,ans2 = max(a,b)。另有特殊情况,当共有一个强连通分量时,ans2=0。
我们可以先进行缩点求出dag图,然后我们考虑第一个问题,求最少发几套软件可以全覆盖,首先题意已经保证了是联通的。然后我们可以想,如果我们把所有没有入边的点都放上软件,是一定可行的。有入边的一定会通过一些边最终从一定有出边的发放软件的地方获得软件。
然后我们考虑第二个问题。这是一个连通图。如果我们有些点没有入点,有些点没出点。那我们如果想办法将入点和一些出点相连,就能保证最后会成为很多圆相连。这样子答案就是没有入边的点和没有出边的点的最大值。
问题A:在网络中需要多少电脑才能使所有学校都有软件,通过强连通算法缩点,构建DAG图,图中一定有入度为0的节点,由于这种节点无法通过别的节点传送软件,所以必须放一台电脑,求出入度为0的节点个数即可。
问题B:在该网络中最少添加多少线路,使在任意节点放置电脑所有学校都可以有软件,答案是求入度为0的节点个数和出度为0的节点个数的最大值。
证明B:
1.以下所有点都是孤立的,连接两个点时,出发的边连接的是起始点中出度为0的点,终点中入度为0的点。
(1).定义当点数n=1时,如果是缩点构成的一个点必须添加一条反身边,否则不用添加边。
(2).当点数n=2时,连接两个点,缩成一个点,然后按规则(1),可得最少添加边数为2。
(3).当n>2时,通过规则(2)不断缩点,当点缩为一个时按规则(1),可得最少添加边数为n。
2.从最旁边的入度为0的节点出发与其下的最旁边的出度为0节点配对,将途中所有点包括起点和终点缩成一个点,并断开所有与其他节点连接的边,通过这样可以使DAG图形成由n(n>0)个孤立点组成的图,求最少添加边数是该图强连通,则可有1得出最少添加边数为n,由于n个孤立点都是入度为0的节点,出度为0的节点或两者配对形成的,可知n是入度为0的节点个数和出度为0的节点个数的最大值。
1 #include <stdio.h> 2 #include <iostream> 3 #include <string.h> 4 #include <algorithm> 5 #include <stack> 6 #include <string> 7 #include <sstream> 8 using namespace std; 9 const int maxn=1e6+5; // 不能写成#define maxn 1e6+5,会编译错误 10 11 struct Edge{ 12 int v; 13 int next; 14 }Edge[maxn];//边的集合 15 16 int node[maxn];//顶点集合 17 int instack[maxn];//标记是否在stack中 18 int Belong[maxn];//各顶点属于哪个强连通分量 19 int DFN[maxn];//节点u搜索的序号(时间戳) 20 int LOW[maxn];//u或u的子树能够追溯到的最早的栈中节点的序号(时间戳) 21 int n,m;//n:点的个数;m:边的条数 22 int cnt_edge;//边的计数器 23 int Index;//序号(时间戳) 24 int Bcnt; //有多少个强连通分量 25 int out[maxn];//存储出度 26 int in[maxn];//存储入度 27 stack<int> sk; 28 29 void add_edge(int u,int v)//邻接表存储 30 { 31 Edge[cnt_edge].next=node[u]; 32 Edge[cnt_edge].v=v; 33 node[u]=cnt_edge++; 34 } 35 36 void tarjan(int u) 37 { 38 DFN[u]=LOW[u]=++Index; 39 instack[u]=1; 40 sk.push(u); 41 for(int i=node[u];i!=-1;i=Edge[i].next) 42 { 43 int v=Edge[i].v; 44 if(!DFN[v])//如果点v没被访问 45 { 46 tarjan(v); 47 LOW[u]=min(LOW[u],LOW[v]); 48 } 49 else //如果点v已经被访问过 50 { 51 if(instack[v]&&DFN[v]<LOW[u]) 52 LOW[u]=DFN[v]; 53 } 54 } 55 if(DFN[u]==LOW[u]) 56 { 57 Bcnt++; 58 int t; 59 do{ 60 t=sk.top(); 61 sk.pop(); 62 instack[t]=0; 63 Belong[t]=Bcnt; 64 }while(t!=u); 65 } 66 } 67 68 void work() 69 { 70 for(int i=1;i<=n;i++) 71 { 72 for(int j=node[i];j!=-1;j=Edge[j].next) 73 { 74 int v=Edge[j].v; 75 if(Belong[i]!=Belong[v]) 76 { 77 out[Belong[i]]++; 78 in[Belong[v]]++; 79 } 80 } 81 } 82 int RU=0; 83 int CHU=0; 84 for(int i=1;i<=Bcnt;i++) 85 { 86 if(!in[i]) RU++; 87 if(!out[i]) CHU++; 88 } 89 if(Bcnt==1) 90 printf("1\n0\n"); 91 else 92 printf("%d\n%d\n",RU,max(RU,CHU)); 93 } 94 95 int main() 96 { 97 freopen("sample.txt","r",stdin); 98 memset(node,-1,sizeof(node)); 99 scanf("%d",&n); 100 getchar(); 101 for(int i=1;i<=n;i++) 102 { 103 int v; 104 string str; 105 getline(cin,str); 106 istringstream ss(str); 107 while(ss >> v&&v) 108 { 109 add_edge(i,v); 110 } 111 } 112 for(int i=1;i<=n;i++) 113 { 114 if(!DFN[i]) 115 { 116 tarjan(i); 117 } 118 } 119 work(); 120 return 0; 121 }
图之强连通、强连通图、强连通分量 Tarjan算法
原文地址:https://www.cnblogs.com/jiamian/p/11187440.html