树分治

点分治

概念

点分治是一种针对可带权树上简单路径统计的算法, 适合处理大规模的树上路径信息问题.

注意: 对于树上路径, 我们不要求这棵树有根, 即我们只需要对于无根树进行统计.

例题引入

【模板】点分治 1

我们先任意选择一个结点作为根节点 \(rt\), 所有完全位于其子树中的路径可以分为两种: 一种是经过根节点的, 一类是不经过根节点的. 而对于经过根节点的路径, 我们又可以分为两种: 某一端点是根节点和两个端点均不是根节点的. 可以发现, 后者可以由两个前者合并得到. 于是对于枚举的根节点 \(rt\), 我们先计算在其子树中且经过该节点的路径对于答案的贡献, 在递归其子树对不经过该节点的路径进行求解.

在本题中, 让我们求出距离为 \(k\) 的路径是否存在. 所以我们可以对于经过 \(rt\) 的路径, 先枚举其所有子节点 \(son\), 又以 \(son\) 为根计算 \(son\) 子树中所有节点到 \(rt\) 的距离. 不妨设结点 \(i\) 到当前根节点 \(rt\) 的距离为 \(dis_i\), \(tf_d\) 表示之前处理过的所有子树中是否存在一个结点 \(x\), 使得 \(dis_x = d\). 如果一个询问 \(k\) 满足 \(tf_{k - dis_i} = 1\), 则存在一条长度为 \(k\) 的路径. 在计算完 \(son\) 子树中所连的边是否能够成为答案后, 我们将这些新的距离加入 \(tf\) 数组中.

注意在清空 \(vis\) 数组的时候不能直接 memset, 而是应将之前占用过的 \(vis\) 位置加入一个队列中, 进行清空. 从而保证时间复杂度.

点分治过程中, 每一层的所有递归过程对每个点均处理了一次, 假设共递归 \(h\) 层, 则时间复杂度为 \(\mathcal{O}(hn)\).

但是如果我们选择树的重心作为根节点, 可以保证递归层数最少, 时间复杂度 \(\mathcal{O} (n \log^2 n)\).

//
// Created by Kang Lingrui on 2025/1/24.
//
#include "iostream"
#include "vector"
#include "stack"

using namespace std;

typedef pair<int, int> pii;

constexpr int N = 1e4 + 10, M = 1e2 + 10, SZ = 1e7 + 10, INF = 1e9;

int n, m, q[M];
vector<pii> e[N];

void init() {
    scanf("%d %d", &n, &m);
    for (int i = 1, u, v, w; i ^ n; ++i) {
        scanf("%d %d %d", &u, &v, &w);
        e[u].emplace_back(v, w);
        e[v].emplace_back(u, w);
    }
    for (int i = 1; i <= m; ++i) scanf("%d", q + i);
}

bool vis[N];
int rt = 0, sum, sz[N], mx[N];

void find_rt(int u, int fa) {
    sz[u] = 1, mx[u] = 0;
    for (auto [v, w] : e[u]) {
        if (v == fa or vis[v]) continue;
        find_rt(v, u);
        sz[u] += sz[v], mx[u] = max(mx[u], sz[v]);
    }
    mx[u] = max(mx[u], sum - sz[u]);
    if (mx[u] < mx[rt]) rt = u;
}

int dis[N], d[N], cnt;

void deal_dis(int u, int fa) {
    d[++cnt] = dis[u];
    for (auto [v, w] : e[u]) {
        if (vis[v] or v == fa) continue;
        dis[v] = dis[u] + w;
        deal_dis(v, u);
    }
}

bool tf[SZ];
int ans[M];

void dfs(int u, int fa) {
    stack<int> st;
    st.push(0);
    tf[0] = vis[u] = true;
    for (auto [v, w] : e[u]) {
        if (vis[v] or v == fa) continue;
        dis[v] = w, deal_dis(v, u);
        for (int i = 1; i <= cnt; ++i)
            for (int j = 1; j <= m; ++j)
                if (q[j] >= d[i]) ans[j] |= tf[q[j] - d[i]];
        for (int i  = 1; i <= cnt; ++i)
            if (d[i] <= 1e7) st.push(d[i]), tf[d[i]] = true;
        cnt = 0;
    }
    while (!st.empty()) tf[st.top()] = false, st.pop();
    for (auto [v, w] : e[u]) {
        if (vis[v] or v == fa) continue;
        sum = sz[v], mx[rt = 0] = INF;
        find_rt(v, u), find_rt(rt, -1);
        dfs(rt, u);
    }
}

void calculate() {
    mx[rt] = INF, sum = n;
    find_rt(1, -1), find_rt(rt, -1);
    dfs(rt, -1);
    for (int i = 1; i <= m; ++i) puts(ans[i] ? "AYE" : "NAY");
}

void solve() {
    init();
    calculate();
}

int main() {
    solve();
    return 0;
}

动态点分治 / 点分树

用来解决带修改的树上路径信息统计问题.

前置知识

点分治.

点分树

先来回顾一下点分治: 先处理该树中的答案, 再找到这棵树的重心并删去, 然后递归处理剩下的子树.

而点分树可以看做将点分治的过程给记录下来, 将当前树的重心与上一级树的重心之间连边, 这样可以得到一个形态比较优秀的重构树, 同时就可以用比较优秀的复杂度解决与树的形态无关的一类问题.

pictures.jpg

重构树有以下几个性质:

  • 其高度与点分治高度相同, 是 \(\log n\) 级别的. 这使得我们可以在这棵重构树上搞出各种各样的暴力做法.
  • 对于任意两点 \(u, v\), 唯一可以确定的是 \(u, v\) 在点分树上的 \(\rm{lca}\) 一定在 \(u \to v\) 的路径上. 换句话说, \(dis(u, v) = dis(u, \textrm{lca}) + dis(\textrm{lca}, v)\).

例题: 【模板】点分树 | 震波

题意

动态维护对于某个点距离小于等于 \(k\) 的点权和. 强制在线.

思路

先考虑暴力. 我们可以在每次修改后都跑一次点分治, 那么每次我们找到重心统计答案即可.

可以发现, 对于每次暴力, 我们都会有找重心的操作, 这是重复的. 事实上, 我们只需要对于每一个重心将答案记录下来即可.

模拟一下点分治的过程: 从整棵树的重心开始, 一步一步递归分治处理到当前点. 将这样的过程放到点分树上, 事实上就是遍历根节点到当前节点路径上的所有点.

现在考虑如何统计答案. 假设当前点为 \(x\).

最开始在 \(x\) 的时候, 直接将 \(x\) 子树内的贡献加上即可. 再来考虑在跳父亲过程中的贡献 (设当前节点为 \(u\), 与 \(x\) 的距离为 \(dis\)), 这时候我们需要计算的就是\(u\) 子树内但不在 \(x\) 子树内点的贡献. 换句话说, 也就是「在 \(u\) 子树内但不在 \(x\) 子树内的点」与 \(x\) 的距离 \(\le y - dis\) 的节点的权值和. 容斥一下, 可以转化为「在 \(u\) 子树内的点」与 \(u\) 的距离 \(\le y - dis\) 的节点 - 「在 \(x\) 子树内的点」与 \(u\) 的距离 \(\le y - dis\) 的节点的权值和.

我们记 \(W_{i, j}\) 表示 \(i\) 子树内与 \(i\) 距离为 \(j\) 的节点的权值和, 那么每次我们求的就是一个前缀, 用树状数组优化一下就行了.

综上所述, 我们需要用到的信息有两个:

  1. \(i\) 子树内节点对 \(i\) 的贡献.
  2. \(i\) 子树内节点对 \(fa_i\) 的贡献.

分别开树状数组记录即可.

代码 link.

posted @   Steven1013  阅读(8)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示