「Dominator Tree」
参考 https://www.cnblogs.com/meowww/p/6475952.html 。
本文主要用于理清证明的思路(也就是说全是口胡 + 不会有详细的关于算法本身的讲解),严谨证明见上。
给定有向图及源点 (假设 能到达所有点),若 到 的所有路径都经过 ,则称 支配 。
我们不加证明地指出:存在唯一一棵以 为根的有根树,使得 支配 当且仅当 在树上是 的非严格祖先(可以是 本身),称其为支配树。
其中,每个点 在支配树上的父亲称为 的最近支配点(immediate dominator),记作 。
如果是 DAG,支配树可以按拓扑序求,求 只需求 的所有入点在已知的支配树上的 lca。
下文将介绍求一般图的支配树的 Lengauer-Tarjan 算法。
先求出以 为根的 dfs 树。在点之间定义序关系 ,表示 在 dfs 序中比 靠前(注意不是编号大小,下文中比较大小都是按这个比)。
此时所有边可以分为几类:
- 树边(Tree Edge)。
- 前向边(Forward Edge):指向子树内的非树边。
- 后向边(Back Edge):指向祖先的非树边。
- 横叉边(Cross Edge):其他非树边。
其中 1,2 是小连向大而 3,4 是大连向小。
一个重要的观察:小连向大的边只有祖先向后代的边,而没有跨子树的边。得到如下结论:
路径引理:
若 ,则 到 的任意路径都经过 的某个公共祖先(注意不一定是 LCA)。
等价于删去所有公共祖先后 无法到达 。
利用路径引理分析 到某点 的某条简单路径的结构:
记该路径 最后一个 的祖先为 ,将路径分为 与 两部分。
由引理知 经过的点(除起点 与终点 )都应该 ,否则 不是最后一个祖先。
引入记号:如果 是 的祖先,且存在一条路径 使得经过的点(除起点 与终点 )都 ,则记 。
注:如果直接存在一条边 ,根据定义,也有 。
而 部分可以递归拆解,由此得到如下结论:
建立新图 :如果 ,则在 中连边 。则 的支配树与 的支配树相同。
注意 保留了原图的 dfs 树,并且只剩下祖先连向后代的边。虽然 在实际操作时没啥用,不过它有助于你理解下文中的 。
定义所有 中最小的 为 的半支配点(semi-dominator),记作 。
对于半支配点有如下结论存在:
记 表示 在 dfs 树上的父亲。
建立新图 :连边 。则 与 的支配树相同。
口胡的证明:
考虑在 上检验 的每个祖先 是否支配 ,即是否有路径可以绕过 到达 。
该路径形如 ,其中 且存在边 。
那么 越小越优,直接取 最优,因此在 中除了树边(用于维持祖先关系)只有 有用。
考虑怎么求出 。
按 dfs 序倒序求。对于每个点 ,考虑 对应路径的最后一条边 。
如果 ,直接用 更新 。
否则,取 这条链(不含 )上的所有点的 去更新 。
容易发现该做法的正确性。
并查集维护即可。如果你写过离线 lca 的 tarjan 算法,应该可以快速 get 到这个点。
接下来,如何已知 求 。
当然,可以套 DAG 的咸鱼做法,得到 的最终复杂度。
由于我们求 时写的是并查集(虽然但是,不带按秩合并的并查集的复杂度也带 log),考虑能否沿用这一算法。
求出 这条链(不含 )上所有点 最小的 ,依然可以用并查集求。
如果 ,则 ;
否则,。
口胡的证明:
首先证求出来的 以下所有点都可以被绕过,然后证该点不能被绕过即可。
由于我们是按 dfs 序倒序来求,然而 的依赖关系是 dfs 序正序,所以最后还要正着扫一遍求出所有 。
参考实现(用于通过 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 的代码,支配树太简单了(确信)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现