【学习笔记】支配树

先对自己说句话:你觉得没用的算法不一定没用,别太自以为是在那里一遍一遍叫 "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\) 矛盾。

类似的,支配还有以下性质:

  1. \(x\) 支配 \(y\)\(y\) 支配 \(x\),则 \(x=y\)

    反证,假如 \(x \ne y\),那么说明 \(s \to x \to y\)\(s \to y \to x\),那么说明 \(s \to y\) 的路径中一定出现了环,而显然路径可以不出现环,矛盾。

  2. \(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\),那么有以下结论:

  1. \(v = u\),那么 \(idom_u = sdom_u\)
  2. 否则 \(sdom_u = sdom_v\)

证明太长了,真的看不下去了,咕了。

posted @ 2023-02-16 22:13  APJifengc  阅读(87)  评论(4编辑  收藏  举报