点分治 & 点分树 笔记
点分治
1. 介绍
点分治常用于解决树上路径数目,符合条件的点对数目的问题。
2. 实现
先看例题:
P4178 Tree
给定一棵
(0). 大体思路
找到一棵树的重心,然后统计经过重心的路径数目,然后再分治所有的子树。
由于找的都是重心,所以时间复杂度
(1). 找根
由于处理重心,所以要先写一个函数找重心。
我们知道,以重心为根,重心最大的子树的大小,是绝对小于以非重心为根,那个点的最大子树大小的,所以我们只需要处理每个点的子树大小
需要注意的是,节点
Code:
void getrt(int u, int fa, int nodecnt) {
sz[u] = 1;
mx[u] = 0;
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].v;
if (v == fa or vis[v]) continue;
getrt(v, u, nodecnt);
sz[u] += sz[v];
mx[u] = max(mx[u], sz[v]);
}
mx[u] = max(mx[u], nodecnt - sz[u]);
if (mx[u] < mx[rt]) rt = u;
}(2). 处理每个点到重心的距离
既然要处理经过重心,且距离小于等于
Code:
void getdis(int u, int fa, int dis) {
q[++p] = dis;
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].v;
if (v == fa or vis[v]) continue;
getdis(v, u, dis + edge[i].w);
}
}(3). 计算+递归
在上一步中,我们把每个点到重心的距离存入了一个
但是,我们发现这样会算重:

如上图所示,如果
我们发现,这些重复的路径都满足经过他的子节点
Code:
int calc() {
int ret = 0;
sort(q + 1, q + p + 1);
for (int i = 1; i < p; i++)
ret += upper_bound(q + i + 1, q + p + 1, k - q[i]) - (q + i + 1);
return ret;
}
void solve(int u, int nodecnt) {
mx[rt = 0] = oo;
getrt(u, 0, nodecnt);
getrt(rt, 0, nodecnt);
p = 0;
getdis(rt, 0, 0);
ans += calc();
vis[rt] = 1;
for (int i = head[rt]; i; i = edge[i].next) {
int v = edge[i].v;
if (vis[v]) continue;
p = 0;
getdis(v, rt, edge[i].w);
ans -= calc();
solve(v, sz[v]);
}
}3. 刷题总结
(1). P4183 [USACO18JAN] Cow at Large P
对于一个点
其中
这样子就转换成了就满足关系的点对数,和上面的例题一样,每次分治只统计满足下面条件的
假设从重心到
即
开两个桶,分别统计,再容斥掉来自同一子树的答案即可。
但是这样会有个问题,对于一个满足条件的点

如图,蓝色填充区域都被重复计算了。
也就是说,我们希望得到一种差分,给每个节点安上一个点权,使得对于每个子树,子树的总权值和都为
假设当前
(2). P2664 树上游戏
考虑转换为点对关系问题,对于下面的一棵树,

显然,对于每个
那
那
也就是说,钦定一棵子树里的点作为出发的,其它子树内一种颜色只有一个节点有贡献,这个有贡献的节点是离重心最近的,于是就可以统计出每种颜色的贡献
对钦定子树进行统计时,先统计出总颜色贡献
此外,这种统计方法还没有考虑从重心到钦定子树的节点
接下来还有重心颜色的问题,还是暴力数出重心的连通块中,重心颜色仅出现一次的最大连通块大小,判断从重心走到

(是的又是这张图……)
假设黑色节点其实是蓝色的,那么红色边上的节点都应该加上红色连通块大小(不包含蓝点)的贡献。
十分的复杂,但仔细想一下还是比较板的,就是分为三类讨论:贡献来自子树外,贡献来自子树内,贡献来自重心。
(3). P3714 [BJOI2017] 树的难题
这个就相对简单了,首先从重心出发往下的颜色序列的贡献是易于统计的,关键就在于跨越重心两端的边颜色是否相同。
其实,可以直接把每个节点的儿子按边颜色排序,颜色相同的一起处理即可。
接着还有距离问题,这就是一个区间查询,线段树维护即可。
总结,就是说开两个线段树,一个维护重心左右儿子颜色不同的情况,另一个维护重心左右儿子颜色相同的答案。
点分树
又叫“动态点分治”,实际上与点分治的关系只有建树方式相同,常用于带修树上问题。
1. 例题
还是拿一道例题来讲:P6329 【模板】点分树 | 震波。
什么是点分树呢,就是把重心串起来得到一棵新树。

这样子,树高就变成了
考虑只求子树内距离不超过
那么点分树上也是类似的,借用点分治的“跨越重心”思想,统计离
- 统计自己点分树子树内的答案。

- 统计点分树父亲节点(黄色)中,扣掉自己这边子树(蓝色)内的答案。

- 往上跳。

这样子,点分树算法的流程就明了了。
但是有个问题,点分树的父亲和儿子关系几乎微乎其微,不能用父亲的线段树减去儿子的线段树得到其它子树的答案,对于每个节点,只需要另外维护一个线段树,表示这个节点子树内和这个节点点分树上父亲的关系的即可。
核心代码(建树跑一遍点分治,把每个节点对应点分树上的父亲记录下来即可)。
t 是统计子树内的线段树,tf 是统计子树内和子树根点分树上父亲关系的线段树。
void update(int x, int v) {
int u = x;
while (u) {
t.update(t.root[u], 0, n - 1, dis(u, x), v);
if (dfa[u])
tf.update(tf.root[u], 0, n - 1, dis(dfa[u], x), v);
u = dfa[u];
}
}
int query(int x, int v) {
int u = x, pre = 0, ret = 0;
while (u) {
if (dis(u, x) <= v)
ret += t.query(t.root[u], 0, n - 1, 0, v - dis(u, x)) - tf.query(tf.root[pre], 0, n - 1, 0, v - dis(u, x));
pre = u, u = dfa[u];
}
return ret;
}2. 刷题总结
(1). P4115 Qtree4
前置知识:可并堆
multiset 的常数太大了,有没有什么常数小的实现方法?
或者说写一个桶,满足以下功能:取最大值,添加一个数,删除其中任意一个数。
只需要写一个原桶,写一个删除桶,用优先队列实现,如果原桶的桶顶和删除桶的桶顶一致,就同时弹出。
struct DUI {
priority_queue<int> q, d;
int sz;
void add(int x) {
q.push(x), sz++;
}
void del(int x) {
d.push(x), sz--;
}
void refresh() {
while (!q.empty() and !d.empty() and q.top() == d.top())
q.pop(), d.pop();
}
int top() {
refresh();
return q.top();
}
};先对每个节点维护一个可并堆
每次更新白点黑点时,依次更新即可,代码有点恶臭但还算易懂。
(2). P3345 [ZJOI2015] 幻想乡战略游戏
考虑如果钦定补给点设在
假设最优点是
(3). P3241 [HNOI2015] 开店
套路还是一样的,无非就是怎么统计的问题,只需要存下子树内每个节点的年龄和距离,对子树内按年龄从小到大排个序,二分找到年龄区间前缀和优化做就完了。
记得加上点分树父亲到当前节点这段距离的贡献。