P5180 【模板】支配树
注:摘自 笔记。所有定理均有通俗易懂的证明,且大量配图。
4. 有向图必经点:支配树
前置知识:有向图 DFS 树。
无向图的必经点由割点和割边刻画,有向图同样存在必经点相关的概念与算法:支配点和支配树。学习一般图支配树能够有效提升图论水平!
4.1 定义与性质
首先明确:接下来的所有探究基于 起点固定 这一前提条件。
对一般有向图
对
和无向图一样,在讨论支配关系时,只考虑 简单路径。如果重复经过一个点,那么将这两次经过之间的点删去。被删去且没有在新路径上出现的点不是支配点。
支配是一种二元关系,我们记作
- 传递性:若
xDy ,yDz ,则1\rightsquigarrow z 的任意路径都经过y ,且1\rightsquigarrow y 的任意路径都经过x 。因此,x 在1\rightsquigarrow z 的任意路径上,即xDz 。 - 自反性:
1\rightsquigarrow x 的任意路径都经过x ,即xDx 。 - 反对称性:若
xDy ,yDx ,则1\rightsquigarrow y 的任意路径都经过x 。如果x\neq y ,那么存在1\rightsquigarrow x 的路径不经过y ,和yDx 矛盾。因此x = y 。反对称性 即对称性的反面:对称性要求对于任意x\neq y ,若xDy 则yDx 。
满足以上三条性质的关系称为 偏序关系,元素集合和它对应的偏序关系一起称为偏序集。
补充
偏序关系的含义是 “部分” 满足 “序关系”,即局部满足 全序关系。在全序关系
R 里,元素两两可比较(对x\neq y ,xRy 或yRx 恰有一个成立)。这种关系抽象成图是一排点,每个点指向它后面的所有点。直观地,在一个偏序集内部,元素之间通过偏序关系形成很多条链,链与链之间有交叉。在交叉点以外,分别位于两个不同链的元素不能比较。但是单独取出某一条链,上面的元素形成全序集。
对偏序关系建图只能得到 DAG:如果环长大于
性质 1
若
xDz 且yDz ,则xDy 或yDx 。证明
因为 $xDz$ 且 $yDz$,不妨设 $x$ 在 $y$ 之前,那么 $y$ 不支配 $x$。如果在此基础上 $x$ 不支配 $y$,那么存在一条 $1\rightsquigarrow y\rightsquigarrow z$ 的路径不经过 $x$,与 $xDz$ 矛盾。所以 $xDy$。$\square
当一个偏序关系满足性质 1 时,就可以用树状结构刻画:考虑支配
设
从
4.2 有向无环图
DAG 无环的特殊性质使得我们能够方便地求出其支配树。
拓扑排序。设当前结点为
当
当
根据上述分析,容易证明:若
因为拓扑序在
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1 << 16;
constexpr int K = 16;
constexpr int M = 1e6 + 5;
int p, topo[N], sz[N];
int n, dep[N], deg[N], anc[K][N];
int cnt, hd[N], nxt[M], to[M];
void add(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v;}
int lca(int u, int v) {
if(!u || !v) return u | v;
if(dep[u] < dep[v]) swap(u, v);
for(int i = K - 1; ~i; i--) {
if(dep[anc[i][u]] >= dep[v]) u = anc[i][u];
}
if(u == v) return u;
for(int i = K - 1; ~i; i--) {
if(anc[i][u] != anc[i][v]) {
u = anc[i][u], v = anc[i][v];
}
}
return anc[0][u];
}
int main() {
cin >> n;
for(int i = 1, u; i <= n; i++) {
scanf("%d", &u);
while(u) deg[i]++, add(u, i), scanf("%d", &u);
if(!deg[i]) deg[i] = 1, add(n + 1, i);
}
queue<int> q;
q.push(n + 1), dep[n + 1] = 1;
while(!q.empty()) {
int t = q.front();
q.pop(), topo[++p] = t;
dep[t] = dep[anc[0][t]] + 1;
for(int i = 1; i < K; i++) anc[i][t] = anc[i - 1][anc[i - 1][t]];
for(int i = hd[t]; i; i = nxt[i]) {
int it = to[i];
anc[0][it] = lca(anc[0][it], t);
if(!--deg[it]) q.push(it);
}
}
for(int i = p; i; i--) {
int id = topo[i];
sz[anc[0][id]] += sz[id] + 1;
}
for(int i = 1; i <= n; i++) printf("%d\n", sz[i]);
return 0;
}
4.3 一般图
先考虑朴素做法。和无向图的必经性一样,我们删掉
Thomas Lengauer 和 Robert Tarjan 于 1979 年给出了更快的支配树算法。具体多快呢?
首先求出以
引理 1
在研究强连通分量横叉边的性质的时候,一个基本结论是如果
引理 2
如果
v < u ,那么所有v 到u 的路径都经过d 的祖先。证明
在考虑 DFS 树的时候,一个形象的方法是把儿子按 DFS 顺序从左到右排列。这样,对于两个没有祖先后代关系的点,左边的点的时间戳小于右边的点。
这个引理的一句话证明是,因为只有树边或前向边增加编号,所以从小于
u 的点走到不小于u 的点这一步x\to y 是树边或前向边,那么x 是u 的祖先(如果x < u 且x 不是u 的祖先,那么x 子树内所有点都小于u ),而从v 开始走到某个u 的祖先的过程中,u, v 的 LCA 只会不断变浅。如上图,
v = 6 ,u = 7 ,虚线是非树边。从u 走到某个d = 5 的祖先1 之前的路径用红色标出,之后的路径用蓝色标出。绿色是编号大于u 的点,可以发现能够直接走到它们的编号不大于7 的点只有7 的祖先。红色路径上每个点和7 的 LCA 依次是5, 1, 1 ,这个 LCA 只会变得越来越浅。具体地,设
c 是当前u 和v 的 LCA,初始为d ,当v = c 时停止。从v 出发依次考虑路径上的每一步:
- 如果走树边或前向边,那么
c 不变。- 如果走返祖边且没有停止,那么
c 不变。- 如果走横叉边,那么
c 只会变成c 的祖先,因为能够使得c 变深的v' > v ,但横叉边只会减小时间戳。例如上图中不会出现2, 3 或4 到6 的使得和7 的 LCA 变深的横叉边。所以最终的
c 是d 的祖先,路径一定经过d 的祖先。\square
引理 2 是有向图上路径的强有力而不那么平凡的结论。它可以想象成一个单向的 “屏障”,从
考虑
半支配点
存在路径
u\to p_1 \to \cdots \to p_k \to x 且p_i > x 的最小的x 的祖先u 称为x 的 半支配点 (semidominator),记为sdom_x 。
将上述定义的 “
为方便描述,对
已知
如上图,
引理 3
引理 3 可以理解为
具体地,
以上分析表明被二元组跳过的点一定不是支配点。那么没有被任何二元组跳过的点是否一定是支配点呢?
引理 4
对
x 的真祖先y ,如果不存在x 的祖先u 使得y 落在sdom_u 到u 在树上的路径上(不含两端),那么y 支配x 。证明
假设存在
1 到x 的不经过y 的路径。考虑路径上所有x 的祖先,可知存在u 到v 的路径P ,使得路径上不经过其它x 的祖先,其中u, v 都是x 的祖先且u 是y 的真祖先,v 是y 的真后代。假设
P 中途经过了小于v 的点,那么根据引理 2,P 在此之后经过了v 的真祖先,和P 不经过其它x 的祖先矛盾。于是P 是好路径。因此sdom_v \leq u 。因为u 是y 的真祖先,所以sdom_v 是y 的真祖先,和y 不落在任何sdom_u 到u 之间矛盾。\square 如上图,蓝色是从
u 出发跳过y 的好路径,红色是从u 出发经过z < v 所以最终一定经过lca(z, v) (x 的小于v 的祖先)的路径。
引理 3 和引理 4 合起来告诉我们一个重要结论:
- 一个等价表述是仅保留树边和
sdom_x 到x 的边不影响支配关系。于是可以使用 DAG 支配树的做法。
设
称添加一个点
结论 1
idom_x = \begin{cases} sdom_x, & \forall p_j : sdom_x \leq sdom_{p_j}; \\ idom_{p_i}, & \exists p_i :(\forall p_j : sdom_{p_i}\leq sdom_{p_j}) \land sdom_{p_i} < sdom_x. \end{cases} 如上图,红色的
idom_{p_i} (可能和sdom_{p_i} 相等)不会被p_j(j > i) 和x 覆盖,而蓝色的p_{i\sim k} 又被x 覆盖,所以idom_x = idom_{p_i} 。
具体如何实现呢?只需计算
现在还要求
如果有中间点,那么根据 DP 的思想,自然地考虑路径的最后一条边
设中途经过的最小点是
对于每条边
结论 2
sdom_x = \min(\{u \mid u < x \land (u, x) \in E\} \cup \{sdom_y \mid y > x \land \exists u : (y\in anc(u) \land (u, x) \in E\})) 其中
anc(u) 表示u 的祖先集合。如上图,蓝色边
u_1\to x 对应的y 用蓝色标出,红色边u_2\to x 对应的y 用红色标出。
后半部分怎么算呢?和
时间复杂度是并查集
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e5 + 5;
int n, m, dn, fa[N], ind[N], dfn[N];
int sdom[N], idom[N], sz[N];
vector<int> e[N], rev[N], buc[N];
void dfs(int id) {
ind[dfn[id] = ++dn] = id;
for(int it : e[id]) if(!dfn[it]) fa[it] = id, dfs(it);
}
struct dsu {
int fa[N], mi[N]; // mi 维护 sdom 最小的点的编号
int find(int x) {
if(fa[x] == x) return fa[x];
int f = fa[x];
fa[x] = find(f);
if(sdom[mi[f]] < sdom[mi[x]]) mi[x] = mi[f];
return fa[x];
}
int get(int x) {
return find(x), mi[x];
}
} tr;
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
rev[v].push_back(u);
}
dfs(1), sdom[0] = n + 1;
for(int i = 1; i <= n; i++) tr.fa[i] = i;
for(int i = n; i; i--) { // i = 1 的时候有些语句不用执行, 不过执行了也没事
int id = ind[i];
for(int it : buc[i]) idom[it] = tr.get(it); // 此时的 idom 是 sdom 最小的 pi
if(i == 1) break;
sdom[id] = i;
for(int it : rev[id]) {
if(dfn[it] <= i) sdom[id] = min(sdom[id], dfn[it]);
else sdom[id] = min(sdom[id], sdom[tr.get(it)]);
}
tr.mi[id] = id, tr.fa[id] = fa[id]; // 连接 id 和 fa[id]
buc[sdom[id]].push_back(id); // 把询问挂到对应位置
}
for(int i = 2; i <= n; i++) {
int id = ind[i];
if(sdom[idom[id]] != sdom[id]) idom[id] = idom[idom[id]]; // 如果 sdom 最小的 sdom[pi] 不等于 sdom[id], 那么 idom[id] = idom[pi]
else idom[id] = sdom[id]; // 否则 idom[id] = sdom[id]
}
for(int i = n; i; i--) {
int id = ind[i];
sz[i] += 1;
if(i > 1) sz[ind[idom[i]]] += sz[i];
}
for(int i = 1; i <= n; i++) cout << sz[i] << " ";
cout << "\n";
return 0;
}