从动态规划的角度理解 Tarjan 算法
- Update on 2023.6.26:重构题解,原题解见 剪贴板。
- Update on 2024.8.9:修改题解。
摘自笔记 图论 I。
无向图 DFS 树
给定无向连通图
给每个点标号为它被访问到的次序,称为 时间戳,简称 dfn。DFS 得到的结点序列称为 DFS 序,时间戳为
上图是一个可能的 DFS 树以及对应的时间戳。
无向图 DFS 树的性质(非常重要):
- 祖先后代性:任意非树边两端具有祖先后代关系。
- 子树独立性:结点的每个儿子的子树之间没有边(和上一条性质等价)。
- 时间戳区间性:子树时间戳为一段区间。
- 时间戳单调性:结点的时间戳小于其子树内结点的时间戳。
Tarjan 求割点
前置知识:DFS 树,DFS 序。
注意区分:DFS 序表示对一张图 DFS 得到的结点序列,而时间戳 dfn 表示每个结点在 DFS 序中的位置。
记
不妨认为
笔者希望提出一种新的理解 Tarjan 算法的方式。网上大部分博客讲解 Tarjan 算法时 low 数组凭空出现,抽象的定义让很多初学者摸不着头脑,从提出问题到解决问题的逻辑链的不完整性让我们无法感受到究竟是怎样的灵感启发了这一算法的诞生。
非根结点的割点判定
设
若
反之,若删去
现在要刻画 “不经过
注意到,如果
进一步地,因为
因此,如果
-
对于
T(y') ,如果存在u\in T(y') 满足f_u < d_x ,那么删去x 后T(y') 的每个点和T'(x) 均连通:T(y') 内所有点通过树边连通,且u 和T'(x) 某点直接相连。 -
反之,如果
T(y') 内所有点的f 值均不小于d_x ,那么删去x 后T(y') 的每个点和T'(x) 均不连通。因为如果连通,那么总得有一个点能一步连通。
这样,我们得到了非根结点的割点判定法则:
这等价于存在 $x$ 的儿子 $y'$,满足 $\min_{u\in T(y')} f_u \geq d_x$。
设 low 的真正含义),根据树形 DP,有
对于后半部分,忽略
说明:将
应用:研究删去
根的割点判定与代码
设
若
综上,使用 Tarjan 算法求无向图
再次强调,以下代码仅在求解割点时正确。求解割边需要额外的特判。
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e5 + 5;
int n, m, R;
int dn, dfn[N], low[N], cnt, buc[N]; // dfn 是时间戳 d, low 是 g
vector<int> e[N];
void dfs(int id) {
dfn[id] = low[id] = ++dn; // 将 low[id] 初始化为 dn 不会导致错误, 且一般都这么写
int son = 0;
for(int it : e[id]) {
if(!dfn[it]) {
son++, dfs(it), low[id] = min(low[id], low[it]);
if(low[it] >= dfn[id] && id != R) cnt += !buc[id], buc[id] = 1;
}
else low[id] = min(low[id], dfn[it]);
}
if(son >= 2 && id == R) cnt += !buc[id], buc[id] = 1;
}
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
for(int i = 1; i <= n; i++) if(!dfn[i]) R = i, dfs(i);
cout << cnt << endl;
for(int i = 1; i <= n; i++) if(buc[i]) cout << i << " ";
return 0;
}
P3469 [POI2008] BLO-Blockade
一道 Tarjan 求割点的练手题。
设删去与结点
因为
时间复杂度
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e5 + 5;
int n, m, dfn[N], low[N], dn;
long long ans[N], sz[N];
vector<int> e[N];
void dfs(int id) {
dfn[id] = low[id] = ++dn;
long long r = n - 1;
ans[id] = r, sz[id] = 1;
for(int it : e[id]) {
if(!dfn[it]) {
dfs(it), low[id] = min(low[id], low[it]), sz[id] += sz[it];
if(low[it] >= dfn[id]) ans[id] += sz[it] * (n - sz[it]), r -= sz[it];
}
else low[id] = min(low[id], dfn[it]);
}
ans[id] += r * (n - r);
}
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
dfs(1);
for(int i = 1; i <= n; i++) cout << ans[i] << endl;
return 0;
}