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 点的。所以这两个点一定在两个分量里面,因此他们俩的边一定是一个桥。


版权声明:本文为qq_40329819原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。