首页 > 其他分享 >【学习笔记】支配树

【学习笔记】支配树

时间:2023-02-16 22:23:05浏览次数:50  
标签:sdom 支配 int 笔记 学习 fa MAXN dep

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

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

标签:sdom,支配,int,笔记,学习,fa,MAXN,dep
From: https://www.cnblogs.com/apjifengc/p/17127212.html

相关文章

  • 多模态学习有哪些架构?MBZUAI最新《多模态表示学习》综述,29页详述多模态表示学习的演化
    前言本文回顾了深度多模态学习方法的演变,并讨论了使主干对各种下游任务具有鲁棒性所需的预训练的类型和目标。本文转载自专知 欢迎关注公众号CV技术指南,专注于计......
  • 传统图机器学习的特征工程
    特征工程的意义特征工程是传统图机器学习中的一个重要环节,它可以帮助我们更好地理解图中的节点、连接和全图,从而更好地构建有效的机器学习模型。特征工程可以帮助我们......
  • 面试笔记
    1 用简历争取到更多的面试机会  本不想写这段,但最近我在帮一些同学准备简历时,发现他们虽然在当前公司里能胜任Java开发的工作,但凭简历恐怕无法得到面试机会,或者无法......
  • Git笔记1
    记录一些语法,希望自己看到的时候可以起到复习的作用echo写入echo"Thisisatext">>Filenamebranch分支展示$gitbranch*devmain星号代表当前分支cat......
  • spring security学习笔记
    1.创建springboot工程,添加lombok插件2.引入springsecurity包3.引入MybatisPuls和mysql驱动包 4.密码加密存储 5.PreAuthorize("hasAuthority('test'......
  • 【学习笔记】Spring之AOP
    Spring之AOP什么是AOPAOP(AspectOrientedProgramming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发......
  • QT基础学习 - 总结
    一、学习规划与必要知识点总结1、QT的下载与安装;1)下载:进入官网,下载QT在线下载工具(QT5.15后都必须在线下载):2)安装参考博客: a. (86条消息)Windows10在线安装Qt5.15和......
  • vue学习之-----组件递归调用
    1、关键点2、父组件<template><div><divclass="btn-title"><el-button@click="addRule(ruleDataList.id)">添加父节点</el-button>......
  • c++学习8 动态空间申请
    一动态分配内存的概述在数组一幕中,介绍过数组的长度是事先预定好的,在整个程序中固定不变。但是在实际的编程过程中,往往会发生这种情况:我们并不清楚到底需要多少数目的空......
  • Day01 Markdown学习
    Markdown学习标题:#+空格+标题:一级标题##+空格+标题:二级标题###+空格+标题:三级标题……最多到六级标题 字体:Hello,World!两个*加粗Hello,World!一个*斜体......