「Dominator Tree」
作者:互联网
参考 https://www.cnblogs.com/meowww/p/6475952.html 。
本文主要用于理清证明的思路(也就是说全是口胡 + 不会有详细的关于算法本身的讲解),严谨证明见上。
给定有向图及源点 \(s\)(假设 \(s\) 能到达所有点),若 \(s\) 到 \(x\) 的所有路径都经过 \(y\),则称 \(y\) 支配 \(x\)。
我们不加证明地指出:存在唯一一棵以 \(s\) 为根的有根树,使得 \(y\) 支配 \(x\) 当且仅当 \(y\) 在树上是 \(x\) 的非严格祖先(可以是 \(x\) 本身),称其为支配树。
其中,每个点 \(x\) 在支配树上的父亲称为 \(x\) 的最近支配点(immediate dominator),记作 \(idom_x\)。
如果是 DAG,支配树可以按拓扑序求,求 \(idom_x\) 只需求 \(x\) 的所有入点在已知的支配树上的 lca。
下文将介绍求一般图的支配树的 Lengauer-Tarjan 算法。
先求出以 \(s\) 为根的 dfs 树。在点之间定义序关系 \(x < y\),表示 \(x\) 在 dfs 序中比 \(y\) 靠前(注意不是编号大小,下文中比较大小都是按这个比)。
此时所有边可以分为几类:
- 树边(Tree Edge)。
- 前向边(Forward Edge):指向子树内的非树边。
- 后向边(Back Edge):指向祖先的非树边。
- 横叉边(Cross Edge):其他非树边。
其中 1,2 是小连向大而 3,4 是大连向小。
一个重要的观察:小连向大的边只有祖先向后代的边,而没有跨子树的边。得到如下结论:
路径引理:
若 \(x < y\),则 \(x\) 到 \(y\) 的任意路径都经过 \(x, y\) 的某个公共祖先(注意不一定是 LCA)。
等价于删去所有公共祖先后 \(x\) 无法到达 \(y\)。
利用路径引理分析 \(s\) 到某点 \(x\) 的某条简单路径的结构:
记该路径 \(s\leadsto x\) 最后一个 \(x\) 的祖先为 \(y\),将路径分为 \(s\leadsto y\) 与 \(y \leadsto x\) 两部分。
由引理知 \(y\leadsto x\) 经过的点(除起点 \(y\) 与终点 \(x\))都应该 \(> x\),否则 \(y\) 不是最后一个祖先。
引入记号:如果 \(y\) 是 \(x\) 的祖先,且存在一条路径 \(y\leadsto x\) 使得经过的点(除起点 \(y\) 与终点 \(x\))都 \(> x\),则记 \(y\mapsto x\)。
注:如果直接存在一条边 \(y\to x\),根据定义,也有 \(y\mapsto x\)。
而 \(s\leadsto y\) 部分可以递归拆解,由此得到如下结论:
建立新图 \(G'\):如果 \(y\mapsto x\),则在 \(G'\) 中连边 \(y\to x\)。则 \(G'\) 的支配树与 \(G\) 的支配树相同。
注意 \(G'\) 保留了原图的 dfs 树,并且只剩下祖先连向后代的边。虽然 \(G'\) 在实际操作时没啥用,不过它有助于你理解下文中的 \(sdom_x\)。
定义所有 \(y\mapsto x\) 中最小的 \(y\) 为 \(x\) 的半支配点(semi-dominator),记作 \(sdom_x\)。
对于半支配点有如下结论存在:
记 \(fa_x\) 表示 \(x\) 在 dfs 树上的父亲。
建立新图 \(G''\):连边 \(fa_x \to x, sdom_x \to x\)。则 \(G''\) 与 \(G\) 的支配树相同。
口胡的证明:
考虑在 \(G'\) 上检验 \(x\) 的每个祖先 \(y\) 是否支配 \(x\),即是否有路径可以绕过 \(y\) 到达 \(x\)。
该路径形如 \(u \leadsto v \leadsto x\),其中 \(u < y < v \leq x\) 且存在边 \(u \to v\)。
那么 \(u\) 越小越优,直接取 \(sdom_v\) 最优,因此在 \(G'\) 中除了树边(用于维持祖先关系)只有 \(sdom\) 有用。
考虑怎么求出 \(sdom\)。
按 dfs 序倒序求。对于每个点 \(x\),考虑 \(sdom_x \mapsto x\) 对应路径的最后一条边 \(z \to x\)。
如果 \(z < x\),直接用 \(z\) 更新 \(sdom_x\)。
否则,取 \(lca(x, z) \leadsto z\) 这条链(不含 \(lca\))上的所有点的 \(\min\{sdom\}\) 去更新 \(sdom_x\)。
容易发现该做法的正确性。
并查集维护即可。如果你写过离线 lca 的 tarjan 算法,应该可以快速 get 到这个点。
接下来,如何已知 \(sdom\) 求 \(idom\)。
当然,可以套 DAG 的咸鱼做法,得到 \(O(n\log n)\) 的最终复杂度。
由于我们求 \(sdom\) 时写的是并查集(虽然但是,不带按秩合并的并查集的复杂度也带 log),考虑能否沿用这一算法。
求出 \(sdom_x \leadsto x\) 这条链(不含 \(sdom_x\))上所有点 \(sdom_p\) 最小的 \(p\),依然可以用并查集求。
如果 \(sdom_p = sdom_x\),则 \(idom_x = sdom_x\);
否则,\(idom_x = idom_p\)。
口胡的证明:
首先证求出来的 \(idom_x\) 以下所有点都可以被绕过,然后证该点不能被绕过即可。
由于我们是按 dfs 序倒序来求,然而 \(idom\) 的依赖关系是 dfs 序正序,所以最后还要正着扫一遍求出所有 \(idom\)。
参考实现(用于通过 https://www.luogu.com.cn/problem/P5180 ):
#include <bits/stdc++.h>
const int N = 200000;
std::vector<int>G[N + 5], R[N + 5], T[N + 5];
void adde(int u, int v) {
G[u].push_back(v), R[v].push_back(u);
}
int dfn[N + 5], tid[N + 5], dcnt;
void dfs1(int x) {
dfn[tid[x] = (++dcnt)] = x;
for(auto to : G[x]) if( !tid[to] )
T[x].push_back(to), dfs1(to);
}
int sdom[N + 5], idom[N + 5], id[N + 5];
int fa[N + 5], mn[N + 5];
int find(int x) {
if( fa[x] == x ) return x;
else {
int f = find(fa[x]);
if( sdom[mn[fa[x]]] < sdom[mn[x]] ) mn[x] = mn[fa[x]];
return fa[x] = f;
}
}
int get(int x) {
find(x); return mn[x];
}
std::vector<int>vec[N + 5];
int siz[N + 5];
int main() {
int n, m; scanf("%d%d", &n, &m);
for(int i=1,u,v;i<=m;i++) scanf("%d%d", &u, &v), adde(u, v);
dfs1(1);
for(int i=1;i<=n;i++) fa[i] = mn[i] = i, sdom[i] = tid[i];
for(int i=n;i>=1;i--) {
int x = dfn[i];
for(auto fr : R[x]) sdom[x] = std::min(sdom[x], sdom[get(fr)]);
vec[dfn[sdom[x]]].push_back(x);
for(auto y : vec[x]) {
if( sdom[get(y)] == sdom[y] ) idom[y] = dfn[sdom[y]];
else id[y] = get(y);
}
for(auto ch : T[x]) fa[ch] = x;
}
for(int i=1;i<=n;i++) if( !idom[dfn[i]] ) idom[dfn[i]] = idom[id[dfn[i]]];
for(int i=n;i>1;i--) siz[idom[dfn[i]]] += (++siz[dfn[i]]); siz[1]++;
for(int i=1;i<=n;i++) printf("%d ", siz[i]);
}
竟然只要 1.3K 的代码,支配树太简单了(确信)。
标签:sdom,支配,int,leadsto,Tree,Dominator,fa,idom 来源: https://www.cnblogs.com/Tiw-Air-OAO/p/14657792.html