tarjan算法求强连通分量数量
一、概念:
1、强连通:
在一个有向图G里,设两个点a b发现,由a有一条路可以走到b,由b又有一条路可以走到a,我们就叫这两个顶点(a,b)强连通。
2、强连通图:
如果在一个有向图G中,每两个点都强连通,我们就叫这个图为强连通图。
3、强连通分量:
在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做强连通分量
举个栗子:
在上图中,x1,x2,x3虽然围成了一个类似环的结构,但均不能互相到达,所以这三个点构成了三个强连通分量,当然x4由于可以到达自己,所以我们也把它称为一个强连通分量,因此这张有向图的强连通分量的个数为4。
二、tarjan算法
tarjan是一个基于Dfs的算法,假设我们要先从0号节点开始Dfs,我们发现一次Dfs就能遍历整个图(树),而且在Dfs的过程中,我们深搜到了其他强连通分量中,那么我们Dfs之后如何判断它的那些节点属于一个强连通分量呢?这便是tarjan算法的核心。首先引入两个数组:
1、dfn[ ]:表示i节点在图中被搜索的序号
2、low[ ]:表示i节点在这颗图中的最小子树的根,如果它自己的low最小,那这个点就应该从新分配,变成这个强连通分量子图的根节点。
-----------------------------------------------------------------------------
3、我们还需要一个队列来维护搜索过程
4、外加vis[i]记录i点是否被压入过队列
接下来我们不妨生成一个有向图模拟一下全过程
先贴个核心代码,不慌,我们慢慢来
void tarjan(int x) {
dfn[x] = low[x] = ++tim;
s[++top] = x; vis[x] = 1;//这里我们用s数组代替了栈
for(int i = 0; i < V[x].size(); ++i) {//枚举子节点
int v = V[x][i];
if(!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
}
else if(vis[v])
low[x] = min(low[x], dfn[v]);
}
if(low[x] == dfn[x]) {
++ans;
while(1) {
int t = s[top--];
vis[t] = 0;
if(t == x) break;
}
}
}
(1)首先我们从x1节点开始Dfs:记录dfn[x1] = low[x1] = 1表示第一次深搜到x1节点,x1节点入队,vis[x1] = true;
(2)Dfs到x3节点,我们记录dfn[x3] = low[x3] = 2表示第二次深搜到x3节点,x2节点入队,vis[x3] = true;
(3)Dfs到x5节点,我们记录dfn[x5] = low[x5] = 3表示第三次深搜到x2节点,x2节点入队,vis[x5] = true;
(4)Dfs到x6节点,记录dfn[x6] = low[x6] = 4,x6节点入队,vis[x6] = true,但当我们想继续深搜下去时发现x6节点的出度为0,那么我们就跳过了for循环,接下来low与dfn变派上用场啦:
判断强连通分量的根节点:dfn[x6] == low[x6],说明x6节点为一个强连通分量的根节点,于是ans++,我们同时在栈中弹出x6节点,返回节点x5,则更新low[x5] = min(low[x5], low[x6]) = 3,Dfs x5;
(5)进入x5节点的循环:
我们突然发现x5的唯一子节点的vis值为true,按照if判断接下来就对x5的low值进行更新:low[x5] = min(low[x5], dfn[x6]) = 3,然后同上判断dfn[x5] == low[x5] == 3,说明x5节点也为一个强连通分量的根节点,那么ans++,弹出x5,返回到x3,更新:low[x3] = min(low[x3], low[x5]) = 2;
(6)Dfs x3先进入x5节点(vis[x5] = true),发现x5已经访问过,那么更新:low[x3] = min(low[x3], dfn[x5]) = 2,接下来进入x4节点,记录dfn[x4] = low[x4] = 5,x4节点入队,vis[x4] = true(现在栈中的元素:x1 x2 x4);
(7)Dfs x4进入x6节点,发现vis[x6] == true,更新:low[x4] = min(low[x4], dfn[x6]) = 4,进入x1节点;
(8)Dfs x4进入x1节点:发现vis[x1] == true,于是又更新x4节点的父子信息:low[x4] = min(low[x4], dfn[x1]) = 1,表示x4为x1的子节点,
循环结束,判断low[x4] != dfn[x4],表示x4虽然在一个强连通分量中,但它并不是一个根节点,那么还是返回x3节点;
(9)返回x3节点low[x3] = min(low[x3], low[x4]) = 1,vis[x4] == vis[x5] == true,两次更新:low[x3] = min(low[x3], dfn(x5)) = 1; low[x3] = min(low[x3], dfn[x4]) = 1;
又判断:low[x3] != dfn[x3],于是又返回x1节点,low[x1] = min(low[x1], low[x3]) = 1(现在栈中的元素:x1);
(10)Dfs x1进入x2节点,记录dfn[x2] = low[x2] = 6,x2节点入队,vis[x2] == true;
(11)Dfs x2进入x4节点,又发现vis[x4] == true并且x4还在栈中,同理更新x2节点的父子信息:low[x2] = min(low[x2], dfn[x4]) = 5,表示x2是x4的一个子节点,返回x2;
(12)Dfs x2,返回x1,更新x1:low[x1] = min(low[x1], low[x2]) = 1,同时发现x1的所有子节点也访问完毕,于是low[x1] = min(low[x1], dfn[x3]) = 1,判断low[x1] == dfn[x1],ans++,栈的最后一个节点x1也终于弹出了,说明以x1为根节点的强连通分量也已经找完了
上图便是整个tarjan的过程,红箭头代表第一次Dfs,黄箭头第二次,紫箭头第三次,棕箭头最后一次,大家可以对着这幅图更深刻地理解~
最后贴一下全代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
int low[maxn], dfn[maxn], vis[maxn];
vector <int> V[maxn];//存图
int ans = 0;
int s[maxn];
int top = 0, tim = 0;
void tarjan(int x) {
dfn[x] = low[x] = ++tim;
s[++top] = x; vis[x] = 1;//这里我们用s数组代替了栈
for(int i = 0; i < V[x].size(); ++i) {//枚举子节点
int v = V[x][i];
if(!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
}
else if(vis[v])
low[x] = min(low[x], dfn[v]);
}
if(low[x] == dfn[x]) {
++ans;
while(1) {
int t = s[top--];
vis[t] = 0;
if(t == x) break;
}
}
}
int main() {
int n, m;
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
V[x].push_back(y);
}
tarjan(1);
cout << ans << "\n";
return 0;
}