「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\) 靠前(注意不是编号大小,下文中比较大小都是按这个比)。

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

  1. 树边(Tree Edge)。
  2. 前向边(Forward Edge):指向子树内的非树边。
  3. 后向边(Back Edge):指向祖先的非树边。
  4. 横叉边(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 的代码,支配树太简单了(确信)。

posted @ 2021-04-16 14:45  Tiw_Air_OAO  阅读(292)  评论(0编辑  收藏  举报