「Dominator Tree」

参考 https://www.cnblogs.com/meowww/p/6475952.html

本文主要用于理清证明的思路(也就是说全是口胡 + 不会有详细的关于算法本身的讲解),严谨证明见上。


给定有向图及源点 s(假设 s 能到达所有点),若 sx 的所有路径都经过 y,则称 y 支配 x

我们不加证明地指出:存在唯一一棵以 s 为根的有根树,使得 y 支配 x 当且仅当 y 在树上是 x 的非严格祖先(可以是 x 本身),称其为支配树。

其中,每个点 x 在支配树上的父亲称为 x最近支配点(immediate dominator),记作 idomx


如果是 DAG,支配树可以按拓扑序求,求 idomx 只需求 x 的所有入点在已知的支配树上的 lca。

下文将介绍求一般图的支配树的 Lengauer-Tarjan 算法。


先求出以 s 为根的 dfs 树。在点之间定义序关系 x<y,表示 x 在 dfs 序中比 y 靠前(注意不是编号大小,下文中比较大小都是按这个比)。

此时所有边可以分为几类:

  1. 树边(Tree Edge)。
  2. 前向边(Forward Edge):指向子树内的非树边。
  3. 后向边(Back Edge):指向祖先的非树边。
  4. 横叉边(Cross Edge):其他非树边。

其中 1,2 是小连向大而 3,4 是大连向小。

一个重要的观察:小连向大的边只有祖先向后代的边,而没有跨子树的边。得到如下结论:

路径引理

x<y,则 xy 的任意路径都经过 x,y 的某个公共祖先(注意不一定是 LCA)。

等价于删去所有公共祖先后 x 无法到达 y


利用路径引理分析 s 到某点 x 的某条简单路径的结构:

记该路径 sx 最后一个 x 的祖先为 y,将路径分为 syyx 两部分。

由引理知 yx 经过的点(除起点 y 与终点 x)都应该 >x,否则 y 不是最后一个祖先。

引入记号:如果 yx 的祖先,且存在一条路径 yx 使得经过的点(除起点 y 与终点 x)都 >x,则记 yx

注:如果直接存在一条边 yx,根据定义,也有 yx

sy 部分可以递归拆解,由此得到如下结论:

建立新图 G:如果 yx,则在 G 中连边 yx。则 G 的支配树与 G 的支配树相同。

注意 G 保留了原图的 dfs 树,并且只剩下祖先连向后代的边。虽然 G 在实际操作时没啥用,不过它有助于你理解下文中的 sdomx


定义所有 yx 中最小的 yx半支配点(semi-dominator),记作 sdomx

对于半支配点有如下结论存在:

fax 表示 x 在 dfs 树上的父亲。

建立新图 G:连边 faxx,sdomxx。则 GG 的支配树相同。


口胡的证明:

考虑在 G 上检验 x 的每个祖先 y 是否支配 x,即是否有路径可以绕过 y 到达 x

该路径形如 uvx,其中 u<y<vx 且存在边 uv

那么 u 越小越优,直接取 sdomv 最优,因此在 G 中除了树边(用于维持祖先关系)只有 sdom 有用。


考虑怎么求出 sdom

按 dfs 序倒序求。对于每个点 x,考虑 sdomxx 对应路径的最后一条边 zx

如果 z<x,直接用 z 更新 sdomx

否则,取 lca(x,z)z 这条链(不含 lca)上的所有点的 min{sdom} 去更新 sdomx

容易发现该做法的正确性。

并查集维护即可。如果你写过离线 lca 的 tarjan 算法,应该可以快速 get 到这个点。


接下来,如何已知 sdomidom

当然,可以套 DAG 的咸鱼做法,得到 O(nlogn) 的最终复杂度。

由于我们求 sdom 时写的是并查集(虽然但是,不带按秩合并的并查集的复杂度也带 log),考虑能否沿用这一算法。

求出 sdomxx 这条链(不含 sdomx)上所有点 sdomp 最小的 p,依然可以用并查集求。

如果 sdomp=sdomx,则 idomx=sdomx

否则,idomx=idomp


口胡的证明:

首先证求出来的 idomx 以下所有点都可以被绕过,然后证该点不能被绕过即可。

由于我们是按 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 的代码,支配树太简单了(确信)。

posted @   Tiw_Air_OAO  阅读(364)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示