【学习笔记】支配树

先对自己说句话:你觉得没用的算法不一定没用,别太自以为是在那里一遍一遍叫 "stop learning useless algorithm",最 useless 的是你。

支配

给定一个有向图 G,有一个起点 s。称 u 支配 v,当且仅当 sv 的路径中必须经过 u

首先我们将 s 无法到达的点删去,讨论这些点的支配关系没有意义。

我们容易发现支配存在传递关系:如果 u 支配 vv 支配 w,那么 u 支配 w

反证,假如 u 不支配 w,那么就说明存在一条 svw 的路径且不经过 u,这与 u 支配 v 矛盾。

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

  1. x 支配 yy 支配 x,则 x=y

    反证,假如 xy,那么说明 sxysyx,那么说明 sy 的路径中一定出现了环,而显然路径可以不出现环,矛盾。

  2. x,y,z 互不相等,且 x 支配 zy 支配 z,那么 x 支配 yy 支配 x

    反证,假如 xy 不存在支配关系,那么存在路径 sxsy,而 x,y 支配 z 说明 xz,yz,这说明存在路径 sxzsyz,这与 x 支配 zy 支配 z 矛盾。

我们设 u 的所有支配点的集合为 Su,那么这个 Su 存在偏序关系。

容易发现,对于每个 xs,存在一个点 z,满足对于任意 yx 支配 x,都有 y 支配 z,我们把这个点 z 称作 x 的直接支配点,记作 z=idom(x)

那么,我们从 idom(x)x 连一条边,就会形成一颗树,这棵树叫做支配树

根据传递性,说明假如在这棵树上 xy 的祖先,那么 x 支配 y

DAG 上的支配树

例题:[ZJOI2012]灾难

考虑在 DAG 上的支配关系:假如某个点 uv 的支配点,那么 u 一定支配 v 的所有前驱。

反证,如果 u 不支配 v 的某个前驱 c,那么存在路径 scv,与 u 支配 v 矛盾。

那么我们考虑对 DAG 进行拓扑排序,那么对于点 uidom(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 序为 dfnu

定义一个点 x 的半支配点 sdomx 为 DFS 序最小的 y,满足存在一条路径 yx,除了 x,y 两个点外所有的点的 DFS 序都比 dfnx 大。

首先可以发现,sdomx 一定是 x 的祖先,因为如果不是 x 的祖先,那么一定有 lca(x,sdomx)sdomx 上的点的 DFS 序也都大于 dfnx,并且 lca(x,sdomx) 的 DFS 序肯定比 sdomx 小,故 sdomx 一定是 x 的祖先。

对于 sdomx 来说,它到 x 存在两条路径,那么说明从 sdomxx 的路径上的所有点(不含 sdomx)肯定不是 x 的直接支配点,那么此时非树边实际上就是为了保证这些点不是直接支配点。如果我们直接把这些非直接支配点删去后,非树边就没有用处了。所以我们可以只连一条从 sdomxx 的边。加上 DFS 树边,这样正好使图变成了一个 DAG。

考虑如何维护 sdomx。考虑枚举 x 的入边 xy,如果 dfnx>dfny,即 yx 的父亲,那么就可以直接更新,否则可以一直跳父亲,取出所有 dfny>dfnx 的点,然后取路径上的 sdomy 最小值更新。

跳父亲显然不可以接受,我们考虑按照 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,令 sdomuu(不含 sdomu)中 sdom 的 DFS 序最小的点为 v,那么有以下结论:

  1. v=u,那么 idomu=sdomu
  2. 否则 sdomu=sdomv

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

posted @   APJifengc  阅读(94)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示