强联通分量——tarjan算法

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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值