Tarjan 算法


解决问题

Tarjan 算法是图论中非常实用 / 常用的算法之一,能解决强连通分量(有向图两点互达),双连通分量(任意两点存在至少两条"边不重复"的路径的图),割点和桥,求最近公共祖先(LCA)等问题。


求强连通分量(缩点)

一些定义

时间戳 d f n dfn dfn :标记图中每个节点在进行深度优先搜索时被访问的时间顺序

追溯值 l o w low low :搜索树中 d f n dfn dfn 最小值

有视频好理解

算法思想

处理一个连通分量时,将访问过的节点存入一个栈中

不断 d f s dfs dfs 进未访问的节点 ( d f n [ y ] = = 0 ) (dfn[y]==0) (dfn[y]==0) ,入栈,向下访问

若节点 y y y 未访问过,则向下访问,同时 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y])

y y y 访问过且在栈 v i s [ y ] = 1 vis[y]=1 vis[y]=1 中,说明在一个连通分量中, l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] / l o w [ y ] ) low[x]=min(low[x],dfn[y]/low[y]) low[x]=min(low[x],dfn[y]/low[y]),这个 / / / 见下面的求割点

结束后若节点 x x x d f n [ x ] = = l o w [ x ] dfn[x]==low[x] dfn[x]==low[x],说明这是以 x x x 为根的强连通分量,将栈中节点一直弹出至 x x x,同时 v i s = 0 vis=0 vis=0,该强连通分量结束

代码

模板

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=1e4+9;
int n,m,tim,cnt,ans;
int a[N],dfn[N],low[N],vis[N],col[N],na[N],r[N],sum[N];
vector<int>e[N],st,ne[N];
queue<int>q;
void tarjan(int x)
{
	st.pb(x); vis[x]=1;
	dfn[x]=low[x]=++tim;
	for(auto y:e[x])
	{
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(vis[y]) low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x])
	{
		++cnt;
		while(1)
		{
			int y=st.back(); st.pop_back();
			col[y]=cnt; vis[y]=0;
			na[cnt]+=a[y];
			if(y==x) break;
		}
	}
}
void topo()
{
	for(int i=1;i<=cnt;i++)
	{
		sum[i]=na[i];
		if(!r[i]) q.push(i);
	}
	while(q.size())
	{
		int x=q.front(); q.pop();
		for(auto y:ne[x])
		{
			sum[y]=max(sum[y],sum[x]+na[y]);
			r[y]--;
			if(!r[y]) q.push(y);
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		e[x].pb(y);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i);
	for(int i=1;i<=n;i++)
		for(auto j:e[i])
			if(col[i]!=col[j])
				ne[col[i]].pb(col[j]),r[col[j]]++;
	topo();
	for(int i=1;i<=cnt;i++)
		ans=max(ans,sum[i]);
	printf("%d\n",ans);
	return 0;
}

割点

算法思想

首先选定一个根节点,从该根节点开始遍历整个图(使用 d f s dfs dfs )。

对于根节点,判断是不是割点很简单——计算其子树数量,如果有 2 2 2 棵即以上的子树,就是割点。

对于非根节点,对于边 ( u , v ) (u, v) (u,v),如果 d f n [ u ] ≤ l o w [ v ] dfn[u]≤low[v] dfn[u]low[v],此式子表明节点 v v v 后面没有边重新回到节点 u u u 所在的连通分量,此时 u u u 就是割点。

同时需要注意的是 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y]),而不能 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y])

l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y]) 的理解

在求强连通分量时,如果 y y y 已经在栈中,那么说明 x , y x,y xy 一定在同一个强连通分量中,所以到最后 l o w [ x ] = l o w [ y ] low[x]=low[y] low[x]=low[y] 是必然的,提前更新也不会有问题。

但是在求割点时, l o w low low 的定义有了小小的变化,不再是最早能追溯到的祖先,(因为是个无向图)没有意义,应该是最早能绕到的割点。

为什么用绕到,是因为是无向边,所以有另一条路可以走,如果把 d f n [ y ] dfn[y] dfn[y] 改成 l o w [ y ] low[y] low[y] 就会上翻过头,可能翻进另一个环中

例如:
在这里插入图片描述
显然对节点 7 7 7,其 l o w low low 不能等于 l o w [ 4 ] low[4] low[4],因为对右边的分量来讲,是从节点 4 4 4 进入这个分量的,能绕回最早的已访问过的节点仍然是 4 4 4,不能从其他边进入左边的分量,对 4 4 4 来说它就是割点

所以 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])

代码

模板

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=2e4+9;
int n,m,tim,ans;
int dfn[N],low[N],gd[N];
vector<int>e[N];
void tarjan(int x,int fa,int rt)
{
	int st=0;
	dfn[x]=low[x]=++tim;
	for(auto y:e[x])
	{
		if(y==fa) continue;
		if(!dfn[y])
		{
			tarjan(y,x,rt); st++;
			low[x]=min(low[x],low[y]);
			if((dfn[x]<=low[y]&&x!=rt)||(x==rt&&st>1)) gd[x]=1;
		}
		else low[x]=min(low[x],dfn[y]);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		e[x].pb(y); e[y].pb(x);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i,0,i);
	for(int i=1;i<=n;i++)
		ans+=gd[i];
	printf("%d\n",ans);
	for(int i=1;i<=n;i++)
		if(gd[i])
			printf("%d ",i);
	return 0;
}

判断条件

对于边 < x , y > <x,y> <x,y>,只要满足 d f n [ x ] < l o w [ y ] dfn[x]<low[y] dfn[x]<low[y] 就证明这条边是一个桥。

为什么呢?根据两个数组的定义, d f n [ x ] < l o w [ y ] dfn[x]<low[y] dfn[x]<low[y] 说明了 y y y 点在不通过 < x , y > <x, y> <x,y> 这条边的情况下,是无论如何也无法到达 x x x 点的。所以这两个点一定在两个分量里面,因此他们俩的边一定是一个桥。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值