Tarjan有关应用更新边的起点到底是用low还是dfn
SpSilverWolf · · 算法·理论
文章目的:
由于我在写 Tarjan 时常常不知道到底是用
先说结论:
在 OI WIKI 上,
设以 $u$ 为根的子树为 $subtree_{u}$。$low_u$ 定义为以下结点的 $dfn$ 的最小值: - $subtree_{u}$ 中的结点; - 从 $subtree_{u}$ 通过一条不在搜索树上的边能到达的结点。
或者说:
从节点
u 出发,沿着 DFS 树向下走,再通过至多一条返祖边(或横叉边)向上跳,能够到达的仍在栈中的节点的最小dfn 值。
所以使用
思想:
对于强联通分量、点双、边双等无论什么应用的树边、前向边、返祖边、横叉边、自环等等无论什么边,到底是用
下面详细讲述每种情况:
由于一般对
由于我只学了 Tarjan 求强连通分量、边双、点双,所以这里只讨论这三种应用。
用语解释:
-
-
-
- “用
low ”或“用dfn ”意为用low_{now}=\min(low_{now},dfn_{i}) 或用low_{now}=\min(low_{now},low_{i}) 来更新边的起点的low 。 - “更新”意为更新边的起点的
low 。 - “边双的两条路径不相交”指路径不共享边,但可以共享顶点。
- “点双的两条路径不相交”指路径不共享除端点以外的顶点。
正片开始:
-
强联通分量:对于每个点,其
low 值为其(标准定义为只经过一条返祖边,不过我觉得去掉这个限制也行)能到达的dfn 值最小的点的dfn 值。 -
树边:此时
i 为now 的儿子。 -
只能用
low ,因为dfn_{i} 必然大于low_{now} ,更新不了,而low_{i} 能到达now ,也即low_{i} 和now 在同一个 SCC,可以更新。 -
前向边:此时
i 为now 的后辈。 -
不用更新,因为
i 已被访问,不用重复更新。 -
返祖边:此时
i 为now 的祖宗。 -
可用
dfn ,此时意为now 和i 在同一个 SCC 内。 -
可用
low ,此时意为可以通过i 走到now 的祖宗(包括父亲,也即i 自己),而now 、i 、low_{i} 都在同一个 SCC 内。 -
用
dfn 或low 都可以,因为无论用哪种更新,得到的low_u 都不会小于u 所在 SCC 的“根”的dfn 。若u 是根,则所有后代能到达的最小dfn 不小于dfn_u ,传递不会使其low 小于dfn_u ;若u 不是根,它的low 值只影响祖先,但同样不会使祖先的low 小于祖先的dfn 。因此两种写法等价。 -
横叉边:此时
i 为now 的亲戚。 -
-
可用
dfn ,此时意为now 和i 在同一个 SCC 内。 -
可用
low ,此时意为可以通过i 走到now 的祖宗(包括父亲,也即i 自己),而now 、i 、low_{i} 都在同一个 SCC 内。 -
用
dfn 或low 都可以,因为无论用哪种更新,得到的low_u 都不会小于u 所在 SCC 的“根”的dfn 。若u 是根,则所有后代能到达的最小dfn 不小于dfn_u ,传递不会使其low 小于dfn_u ;若u 不是根,它的low 值只影响祖先,但同样不会使祖先的low 小于祖先的dfn 。因此两种写法等价。 -
-
边双:目的是找到一个能使其内部任意两点 (
u,v ) 之间都存在至少两条边不相交的路径的极大的子图,所以在已知low_{now} 到达now 已经有一条路径了(就是从low_{now} 搜到now 的路径)的情况下,要找另一条路径,而这条路径要保证与low_{now} 搜到now 的路径不共享边。 -
树边:此时
i 为now 的儿子。 -
只能用
low ,因为dfn_{i} 必然大于low_{now} ,更新不了,而i 到达low_{i} 的路径与low_{i} 搜到now 的路径边必然不相交(因为返祖边和树边方向都不同),所以可以更新。 -
返祖边:此时
i 为now 的祖宗。 -
可用
dfn ,因为该返祖边与i 搜到now 的树边是反向的,不相交,可以更新,此时意为now 和i 在同一个边双内。 -
可用
low ,此时意为可以通过i 走到now 的祖宗(包括父亲,也即i 自己),而该返祖边再接n 段返祖边与low_{i} 搜到now 的树边也不相交(因为方向不同),可以更新,而now 、i 、low_{i} 都在同一个边双内。 -
用
dfn 或low 都可以,因为now 到达dfn_{i} 或low_{i} 的路径与dfn_{i} 或low_{i} 搜到now 的路径不相交,二者都不违反边双的定义。而且边双的判断是low_{i} > dfn_{now} ,而无论用dfn 还是low ,最后得出的low_{i} 都\leq dfn_{now} ,所以不影响边双的判断。 -
点双:目的是找到一个能使其内部任意两点 (
u,v ) 之间都存在至少两条内部不相交的路径的极大的子图,所以在已知low_{now} 到达now 已经有一条路径了(就是从low_{now} 搜到now 的路径)的情况下,要找另一条路径,而这条路径要保证与low_{now} 搜到now 的路径不共享除端点以外的顶点。 -
树边:此时
i 为now 的儿子。 -
只能用
low ,因为dfn_{i} 必然大于low_{now} ,更新不了,而i 到达low_{i} 的路径与low_{i} 搜到now 的路径必然不相交。反证:倘若有交点,则i 到交点的路径上有返祖边,而点双返祖边的更新只能用dfn (下文会证),所以low_{i} 最小是能更新到dfn_{\text{交点}} ,此时这个“交点”就是端点,得证。所以可以更新。 -
返祖边:此时
i 为now 的祖宗。 -
可用
dfn ,因为该返祖边与i 搜到now 的树边只有端点(now 和i )共享,可以更新,此时意为now 和i 在同一个点双内。 -
不可用
low ,此时意为可以通过i 走到now 的祖宗(包括父亲,也即i 自己),而该返祖边再接n 段返祖边与low_{i} 搜到now 的树边有交点,其为返祖边相接的点,与点双的定义不符,now 、i 、low_{i} 不在同一个点双内,所以不能这样更新。
最后贴一下本文基于的模板代码:
Tarjan 模板
#include<bits/stdc++.h>
using namespace std;
int dfn[10005],low[10005],tim;
//dfn:表示点i是全局第几个被访问到的节点,只记录第一次访问时的时间戳,一旦记录不再更改
//low:从节点i向上能访问到的最后一个节点的dfn,当low[i]==dfn[i]时,说明i无法再向根方向访问到\
比它更早进入栈的节点了,说明这个i就是SCC的根
//以上两个变量初始都为0
void Tarjan(int now){
dfn[now]=low[now]=++tim;
//进入一个全新的节点(dfs保证只会进入新结点),记录第一次访问时的时间戳
for(int i:l[now]){
if(!dfn[i])Tarjan(i),low[now]=min(low[now],low[i]);
else if(p[i])low[now]=min(low[now],dfn[i]);}}
int main(){
int n=read(),m=read(),x;
while(m--)x=read(),l[x].push_back(read());
for(int i=1;i<=n;++i)
if(!dfn[i])Tarjan(i);
return 0;}
强连通分量
int a[10005],dfn[10005],low[10005],bel[10005],tim,scc_cnt;
//bel:i属于哪个强连通分量
//tim:全局时间戳,每进入一个全新的节点就+1,然后赋给这个节点
//scc_cnt:分量编号
//以上三个变量初始都为0
bool p[10005];//i是否在栈里
stack<int>st;
vector<int>l[10005];
void dfs(int now){
dfn[now]=low[now]=++tim;
for(int i:l[now]){
if(!dfn[i])dfs(i);//从未访问过(树边)
if(p[i])low[now]=min(low[now],low[i]);}//访问过且在该SCC内(返祖边)
//前向边对SCC不做贡献,忽略
//访问过但不在该SCC内的为横叉边,横叉边单向,两端不互通,故不能更新low
//自环在Tarjan算法中忽略,存图的时候直接不存
if(low[now]==dfn[now]){
++scc_cnt;//知道这一部分是一个强连通分量了
int x;
do{
x=st.top();
st.pop();
p[x]=0;
bel[x]=scc_cnt;
}while(x!=now);}}//把从栈顶一直到根(包括根自己)的全部节点都弹出来,这一堆节点就是一个完整的强连通分量
边双连通分量(e-dcc)
#include<bits/stdc++.h>
using namespace std;
vector<int>l[155];
int dfn[155],low[155],tim;
vector<pair<int,int> >ans;
void dfs(int now,int fa){//只需记录父节点就可避免重复经过之前走过的点
dfn[now]=low[now]=++tim;
for(int i:l[now]){
if(i==fa)continue;
if(dfn[i]==0){
dfs(i,now);
if(low[i]>dfn[now])ans.push_back({min(i,now),max(i,now)});}
low[now]=min(low[now],low[i]);}}
bool cmp(pair<int,int> a,pair<int,int> b){
if(a.first!=b.first)return a.first<b.first;
return a.second<b.second;}
int main(){
int n=read(),m=read();
for(int i=1;i<=m;++i){
int a=read(),b=read();
if(a==b)continue;
l[a].push_back(b),l[b].push_back(a);}
for(int i=1;i<=n;++i)
if(dfn[i]==0)dfs(i,0);
sort(ans.begin(),ans.end(),cmp);
for(auto i:ans)out<<i.first<<' '<<i.second<<'\n';
return 0;}
点双连通分量(v-dcc)
#include<bits/stdc++.h>
using namespace std;
int head[500005],to[4000005],nxt[4000005],tot=1,dfn[500005],low[500005],tim,cnt;
vector<int>ans[500005];
stack<int>st;
void add(int u,int v){
to[++tot]=u;nxt[tot]=head[v];head[v]=tot;}
void dfs(int u,int id){
dfn[u]=low[u]=++tim;
for(int i=head[u];i;i=nxt[i]){
if(i==(id^1))continue;
int v=to[i];
if(!dfn[v]){
st.push(i);
dfs(v,i);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
++cnt;
int x;
do{
x=st.top();st.pop();
ans[cnt].push_back(to[x]);
ans[cnt].push_back(to[x^1]);
}while(x!=i);
sort(ans[cnt].begin(),ans[cnt].end());
ans[cnt].erase(unique(ans[cnt].begin(),ans[cnt].end()),ans[cnt].end());}}
else if(dfn[v]<dfn[u])st.push(i),low[u]=min(low[u],dfn[v]);}}
int main(){
int n=read(),m=read();
for(int i=1;i<=m;++i){
int u=read(),v=read();
if(u==v)continue;
add(u,v),add(v,u);}
for(int i=1;i<=n;++i)if(!dfn[i])dfs(i,0);
for(int i=1;i<=n;++i)if(head[i]==0)ans[++cnt].push_back(i);
out<<cnt<<'\n';
for(int i=1;i<=cnt;++i){
out<<(int)ans[i].size()<<' ';
for(int j:ans[i])out<<j<<' ';
out<<'\n';}
return 0;}