强连通分量总结
概念
来自百度
有向图强连通分量:在有向图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值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。
规则
-
数组初始化:定义两个数组dfn,
dfn[i]
代表第一次到达i时的时间,当首次搜索到点u时,low[i]
初始化为dfn[i]
。 -
堆栈:每遍历到一个未被标记的点u,将它入栈。
-
再从当前点u开始遍历,当从
v
回溯到u
时,如果点v不在栈中,那么low[u] = min{low[u],low[v]}
;如果点v
在栈中,那么low[u] = min{low[u],dfn[v]}
。 -
每当搜索到一个点i并经过以上步骤后,如果其
low[i]=dfn[i]
,则将它以及在它之上的元素弹出栈。这些出栈元素组成一个强连通分量。 -
继续搜索(因为有向图可能有多个连通子图组成,而这些子图没有交集),直到所有点被遍历。
演示
引用网上一篇博客的算法演示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
%d
", ans1, ans2);
return 0;
}
Kosaraju和Gabow
等我想起来,再说吧
总结
还早着呢