【学习笔记】支配树
先对自己说句话:你觉得没用的算法不一定没用,别太自以为是在那里一遍一遍叫 "stop learning useless algorithm",最 useless 的是你。
支配
给定一个有向图 \(G\),有一个起点 \(s\)。称 \(u\) 支配 \(v\),当且仅当 \(s\) 到 \(v\) 的路径中必须经过 \(u\)。
首先我们将 \(s\) 无法到达的点删去,讨论这些点的支配关系没有意义。
我们容易发现支配存在传递关系:如果 \(u\) 支配 \(v\),\(v\) 支配 \(w\),那么 \(u\) 支配 \(w\)。
反证,假如 \(u\) 不支配 \(w\),那么就说明存在一条 \(s \to v \to w\) 的路径且不经过 \(u\),这与 \(u\) 支配 \(v\) 矛盾。
类似的,支配还有以下性质:
- 若 \(x\) 支配 \(y\),\(y\) 支配 \(x\),则 \(x=y\);
反证,假如 \(x \ne y\),那么说明 \(s \to x \to y\) 且 \(s \to y \to x\),那么说明 \(s \to y\) 的路径中一定出现了环,而显然路径可以不出现环,矛盾。
- 若 \(x,y,z\) 互不相等,且 \(x\) 支配 \(z\),\(y\) 支配 \(z\),那么 \(x\) 支配 \(y\) 或 \(y\) 支配 \(x\)。
反证,假如 \(x\) 与 \(y\) 不存在支配关系,那么存在路径 \(s \to x\) 和 \(s \to y\),而 \(x,y\) 支配 \(z\) 说明 \(x \to z, y \to z\),这说明存在路径 \(s \to x \to z\) 和 \(s \to y \to z\),这与 \(x\) 支配 \(z\) 和 \(y\) 支配 \(z\) 矛盾。
我们设 \(u\) 的所有支配点的集合为 \(S_u\),那么这个 \(S_u\) 存在偏序关系。
容易发现,对于每个 \(x \ne s\),存在一个点 \(z\),满足对于任意 \(y \ne x\) 支配 \(x\),都有 \(y\) 支配 \(z\),我们把这个点 \(z\) 称作 \(x\) 的直接支配点,记作 \(z = idom(x)\)。
那么,我们从 \(idom(x)\) 向 \(x\) 连一条边,就会形成一颗树,这棵树叫做支配树。
根据传递性,说明假如在这棵树上 \(x\) 为 \(y\) 的祖先,那么 \(x\) 支配 \(y\)。
DAG 上的支配树
例题:[ZJOI2012]灾难
考虑在 DAG 上的支配关系:假如某个点 \(u\) 是 \(v\) 的支配点,那么 \(u\) 一定支配 \(v\) 的所有前驱。
反证,如果 \(u\) 不支配 \(v\) 的某个前驱 \(c\),那么存在路径 \(s \to c \to v\),与 \(u\) 支配 \(v\) 矛盾。
那么我们考虑对 DAG 进行拓扑排序,那么对于点 \(u\),\(idom(u)\) 就等于它的所有前驱的 LCA。
我们可以使用倍增求 LCA,这样就能动态加点来构造支配树了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 70005;
int n;
vector<int> e[MAXN], re[MAXN];
int deg[MAXN];
int idom[MAXN];
int fa[MAXN][20];
int dep[MAXN];
int lca(int u, int v) {
if (!u || !v) return u + v;
if (dep[u] < dep[v]) swap(u, v);
for (int i = 16; i >= 0; i--)
if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
if (u == v) return v;
for (int i = 16; i >= 0; i--)
if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
vector<int> t[MAXN];
int siz[MAXN];
void dfs(int u) {
siz[u] = 1;
for (int v : t[u])
dfs(v), siz[u] += siz[v];
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int f; scanf("%d", &f);
while (f) {
e[f].push_back(i);
re[i].push_back(f);
scanf("%d", &f);
}
if (re[i].size() == 0) {
re[i].push_back(n + 1);
e[n + 1].push_back(i);
}
deg[i] = re[i].size();
}
queue<int> q;
q.push(n + 1); idom[n + 1] = n + 1, dep[n + 1] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
// printf("%d: \n", u);
for (int v : re[u]) {
// printf("pre %d %d\n", u, v);
idom[u] = lca(idom[u], v);
}
dep[u] = dep[idom[u]] + 1;
fa[u][0] = idom[u];
for (int i = 1; i <= 16; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : e[u]) {
deg[v]--;
if (!deg[v]) q.push(v);
}
}
for (int i = 1; i <= n; i++) {
// printf("idom[%d]=%d\n", i, idom[i]);
t[idom[i]].push_back(i);
}
dfs(n + 1);
for (int i = 1; i <= n; i++) {
printf("%d\n", siz[i] - 1);
}
return 0;
}
一般图支配树
我们首先给这个图跑出来一个 DFS 树。设 \(u\) 的 DFS 序为 \(dfn_u\)。
定义一个点 \(x\) 的半支配点 \(sdom_x\) 为 DFS 序最小的 \(y\),满足存在一条路径 \(y \to x\),除了 \(x, y\) 两个点外所有的点的 DFS 序都比 \(dfn_x\) 大。
首先可以发现,\(sdom_x\) 一定是 \(x\) 的祖先,因为如果不是 \(x\) 的祖先,那么一定有 \(lca(x, sdom_x) \to sdom_x\) 上的点的 DFS 序也都大于 \(dfn_x\),并且 \(lca(x, sdom_x)\) 的 DFS 序肯定比 \(sdom_x\) 小,故 \(sdom_x\) 一定是 \(x\) 的祖先。
对于 \(sdom_x\) 来说,它到 \(x\) 存在两条路径,那么说明从 \(sdom_x\) 到 \(x\) 的路径上的所有点(不含 \(sdom_x\))肯定不是 \(x\) 的直接支配点,那么此时非树边实际上就是为了保证这些点不是直接支配点。如果我们直接把这些非直接支配点删去后,非树边就没有用处了。所以我们可以只连一条从 \(sdom_x\) 到 \(x\) 的边。加上 DFS 树边,这样正好使图变成了一个 DAG。
考虑如何维护 \(sdom_x\)。考虑枚举 \(x\) 的入边 \(x \gets y\),如果 \(dfn_x > dfn_y\),即 \(y\) 是 \(x\) 的父亲,那么就可以直接更新,否则可以一直跳父亲,取出所有 \(dfn_y > dfn_x\) 的点,然后取路径上的 \(sdom_y\) 最小值更新。
跳父亲显然不可以接受,我们考虑按照 DFS 序从大到小枚举,然后直接维护每个点到父亲节点的最小值。可以使用并查集来维护这个东西,在路径压缩的时候与新连接的父亲的权值取 \(\min\) 即可。
主要部分代码:
int find(int x) {
if (f[x] == x) return x;
int fx = f[x];
f[x] = find(f[x]);
val[x] = min(val[x], val[fx]);
return f[x];
}
void solve(int s) {
dfs(s);
for (int i = 1; i <= n; i++)
f[i] = i, val[i] = dfn[i];
for (int i = n; i >= 2; i--) {
int u = idf[i];
for (int v : pre[u]) {
find(v);
val[u] = min(val[u], val[v]);
}
sdom[u] = idf[val[u]];
f[u] = fa[u];
}
for (int i = 1; i <= n; i++) if (i != s) {
dag.add(sdom[i], i);
dag.add(fa[i], i);
}
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n, m;
int idom[MAXN];
int sdom[MAXN];
struct DAG {
vector<int> to[MAXN], pre[MAXN];
int deg[MAXN];
int fa[MAXN][20];
int dep[MAXN];
int lca(int u, int v) {
if (!u || !v) return u + v;
if (dep[u] < dep[v]) swap(u, v);
for (int i = 16; i >= 0; i--)
if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
if (u == v) return v;
for (int i = 16; i >= 0; i--)
if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
void add(int x, int y) {
to[x].push_back(y), pre[y].push_back(x), deg[y]++;
}
void topo(int s) {
queue<int> q;
q.push(s); dep[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : pre[u]) {
idom[u] = lca(idom[u], v);
}
dep[u] = dep[idom[u]] + 1;
fa[u][0] = idom[u];
for (int i = 1; i <= 16; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : to[u]) {
deg[v]--;
if (!deg[v]) q.push(v);
}
}
}
} dag;
struct Graph {
vector<int> to[MAXN], pre[MAXN];
void add(int x, int y) {
to[x].push_back(y), pre[y].push_back(x);
}
int fa[MAXN];
int f[MAXN], val[MAXN];
int find(int x) {
if (f[x] == x) return x;
int fx = f[x];
f[x] = find(f[x]);
val[x] = min(val[x], val[fx]);
return f[x];
}
int dfn[MAXN], idf[MAXN], dcnt;
void dfs(int u) {
dfn[u] = ++dcnt, idf[dcnt] = u;
for (int v : to[u]) if (!dfn[v]) {
dfs(v);
fa[v] = u;
}
}
void solve(int s) {
dfs(s);
for (int i = 1; i <= n; i++)
f[i] = i, val[i] = dfn[i];
for (int i = n; i >= 2; i--) {
int u = idf[i];
for (int v : pre[u]) {
find(v);
val[u] = min(val[u], val[v]);
}
sdom[u] = idf[val[u]];
f[u] = fa[u];
}
for (int i = 1; i <= n; i++) if (i != s) {
dag.add(sdom[i], i);
dag.add(fa[i], i);
}
}
} g;
vector<int> t[MAXN];
void construct(int s) {
g.solve(s);
dag.topo(s);
for (int i = 1; i <= n; i++) if (i != s) {
t[idom[i]].push_back(i);
}
}
int siz[MAXN];
void dfs(int u) {
siz[u] = 1;
for (int v : t[u])
dfs(v), siz[u] += siz[v];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v; scanf("%d%d", &u, &v);
g.add(u, v);
}
construct(1);
dfs(1);
for (int i = 1; i <= n; i++) {
printf("%d ", siz[i]);
}
return 0;
}
另外一个做法:根据一通分析,可以得出一个结论:
对于每个点 \(u\),令 \(sdom_u \to u\)(不含 \(sdom_u\))中 \(sdom\) 的 DFS 序最小的点为 \(v\),那么有以下结论:
- 若 \(v = u\),那么 \(idom_u = sdom_u\);
- 否则 \(sdom_u = sdom_v\)。
证明太长了,真的看不下去了,咕了。