树分治
点分治
概念
点分治是一种针对可带权树上简单路径统计的算法, 适合处理大规模的树上路径信息问题.
注意: 对于树上路径, 我们不要求这棵树有根, 即我们只需要对于无根树进行统计.
例题引入
【模板】点分治 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;
}
动态点分治 / 点分树
用来解决带修改的树上路径信息统计问题.
前置知识
点分治.
点分树
先来回顾一下点分治: 先处理该树中的答案, 再找到这棵树的重心并删去, 然后递归处理剩下的子树.
而点分树可以看做将点分治的过程给记录下来, 将当前树的重心与上一级树的重心之间连边, 这样可以得到一个形态比较优秀的重构树, 同时就可以用比较优秀的复杂度解决与树的形态无关的一类问题.
重构树有以下几个性质:
- 其高度与点分治高度相同, 是 \(\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\) 的节点的权值和, 那么每次我们求的就是一个前缀, 用树状数组优化一下就行了.
综上所述, 我们需要用到的信息有两个:
- \(i\) 子树内节点对 \(i\) 的贡献.
- \(i\) 子树内节点对 \(fa_i\) 的贡献.
分别开树状数组记录即可.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现