文章目录
解决问题
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
x,y 一定在同一个强连通分量中,所以到最后
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 点的。所以这两个点一定在两个分量里面,因此他们俩的边一定是一个桥。