前言
点分治是一种树上分治算法,常用于处理和路径相关的统计,或者是树上满足某种条件的点对数量。
在点分治的基础上,构造一棵支持修改的树,就成了点分树。
算法理解
点分治的核心是按照重心来划分连通块,我们在递归的时候,将上一层的重心作为这一层的重心的父亲,就成了点分树。容易知道,这棵树的树高是 O ( l o g n ) O(logn) O(logn) 的,因为总共只会有 O ( l o g n ) O(logn) O(logn) 层。下面盗用 辰星凌大佬的图 来看看点分树长什么样子。

如图,上面是原树,下面是点分树。可以看到,点分树和原树几乎没有任何关系。但是可以看到的是,点分树的子树点集对应原树上的一个连通块点集。
点分树有一些神奇的性质,让我们一一探讨一下:
- 点分树上某一个点的子树属于同一个连通块
回忆点分治的过程,我们可以知道,当一个点作为点分树子树的根时,它在点分治的时候作为分治中心,而它在点分树上的子树对应的点集和在原树分治时对应的点集相同。 - 点分树所有子树大小之和为 O ( n l o g n ) O(nlogn) O(nlogn)
这是个很奇妙的性质,是点分树用来解决问题的核心。考虑每个点对子树大小和的贡献,因为一个点最多会被 O ( l o g n ) O(logn) O(logn) 个父亲统计到,因此子树大小和为 O ( n l o g n ) O(nlogn) O(nlogn)。
解决问题
点分树可以拿来解决什么问题呢?
比如求 ∑ y = 1 n d i s ( x , y ) \sum\limits_{y=1}^{n}dis(x,y) y=1∑ndis(x,y)。我们考虑一条路径 ( x , y ) (x,y) (x,y),我们可以设一个分界点 p p p,把路径分成 ( x , p ) , ( p , y ) (x,p),(p,y) (x,p),(p,y) 两段,然后分别维护这两段的信息。只不过,在点分树中,我们的 p p p 为分治中心,也就是 ( x , y ) (x,y) (x,y) 在虚树上的 l c a lca lca。
对于任意一个 y y y,我们找到它和 x x x 在虚树上的 l c a lca lca,设为 p p p。可以知道 ( x , y ) (x,y) (x,y) 可以被划分成 ( x , p ) , ( p , y ) (x,p),(p,y) (x,p),(p,y) 两段。
因此我们可以想到一个做法, O ( l o g n ) O(logn) O(logn) 枚举 x x x 的祖先节点,记为 p p p,再来统计 p p p 中 y y y 的贡献。而这种贡献一般通过简单的 容斥原理 求得,具体可以参照下文。
算法实现
点分树·震波
维护一棵带点权树,支持两种操作:修改 x x x 的点权,查询离 x x x 距离不超过 k k k 的点的点权和。
下面用 f a x fa_x fax 表示 x x x 在虚树上的父亲, f a t r e e x fatree_x fatreex 表示 x x x 在虚树上的祖先集合, s u b t r e e x subtree_x subtreex 表示 x x x 在虚树上的子树点集, A x A_x Ax 表示 x x x 的点权。
- 首先建好点分树
- 设 f ( i , j ) = ∑ x ∈ s u b t r e e ( i ) , d i s ( x , i ) ≤ j A x , g ( i , j ) = ∑ x ∈ s u b t r e e ( i ) , d i s ( x , f a i ) ≤ j A x f(i,j)=\sum\limits_{x\in subtree(i),dis(x,i)\leq j}A_x,g(i,j)=\sum\limits_{x\in subtree(i),dis(x,fa_i)\leq j}A_x f(i,j)=x∈subtree(i),dis(x,i)≤j∑Ax,g(i,j)=x∈subtree(i),dis(x,fai)≤j∑Ax。
让我们先观察一下 f f f 和 g g g 有什么不同,可以发现一个是 d i s ( x , i ) dis(x,i) dis(x,i),一个是 d i s ( x , f a i ) dis(x,fa_i) dis(x,fai)。倘若要求在 f a x fa_x fax 的子树中但不在 x x x 的子树中的点对 f a x fa_x fax 的贡献,我们用 g ( x , j ) − f ( x , j − d i s ( x , f a x ) ) g(x,j)-f(x,j-dis(x,fa_x)) g(x,j)−f(x,j−dis(x,fax)) 即可。
这样,在一对查询 ( x , k ) (x,k) (x,k) 中,我们可以得到 a n s = f ( x , k ) + ∑ i ∈ f a t r e e x g ( i , k − d i s ( x , i ) ) − f ( i , k − d i s ( x , i ) ) ans=f(x,k)+\sum\limits_{i\in fatree_x}g(i,k-dis(x,i))-f(i,k-dis(x,i)) ans=f(x,k)+i∈fatreex∑g(i,k−dis(x,i))−f(i,k−dis(x,i)),本质上是考虑 ( x , i ) , ( i , y ) (x,i),(i,y) (x,i),(i,y) 的所有 ( x , y ) (x,y) (x,y) 点对,即下图中的红色部分。

然后,插入的时候,我们 O ( l o g n ) O(logn) O(logn) 爬 x x x 的祖先 i i i,更新 f ( i ) f(i) f(i) 和 g ( i ) g(i) g(i),查询同理。 - 我们接下来要考虑两个问题:
第一,怎么求 d i s dis dis 呢,这个可以树剖求 l c a lca lca 来 O ( l o g n ) O(logn) O(logn) 得到。其实可以使用欧拉序 + ST表来 O ( 1 ) O(1) O(1) 求 l c a lca lca,但考虑到预处理 ST表常数太大了,就用树剖吧。有个更高效的办法是,在点分治找到重心时,直接从这个点 d f s dfs dfs,来得到每个点离祖先的距离,这样就不用在线求距离了。
第二,怎么维护 f f f 和 g g g 呢?注意到这是个前缀和的形式,我们可以考虑使用树状数组。但是,每个点开一个树状数组,会很白给吗?其实是不会的,考虑点分树上 i i i 的子树中的点到 f a i fa_i fai 的距离,设 i i i 的子树大小为 s z i sz_i szi,那么距离取值为 [ 1 , s z i ] [1,sz_i] [1,szi],而 i i i 的子树中的点到 i i i 的距离取值为 [ 0 , s z i 2 ] [0,\frac{sz_i}{2}] [0,2szi],而且下标是从 0 0 0 开始的,因此我们对 i i i 开一个大小为 s z i 2 + 1 \frac{sz_i}{2}+1 2szi+1 的 f f f 和大小为 s z i + 1 sz_i+1 szi+1 的 g g g 即可,总空间是 O ( n l o g n ) O(nlogn) O(nlogn) 的。 - 一些需要注意的点
由于涉及到开空间,子树大小不能弄错,要用正规的点分治
预处理每个点在虚树上的祖先和到祖先的距离的话,需要倒序枚举
一般不需要把虚树的具体形态建出来,且虚树和原树基本没有关系,不要用原树上的边权来代入虚树中。有些初学者可能觉得只用一个树状数组即可,这就犯了这个错误了,因为虚树上的儿子,可能在原树上离父亲十万八千里。
下面给出代码。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5;
vector<int> E[N];
struct Fenwick{
int n;
vector<int> c;
void init(int n_){
n = n_;
c.resize(n_ + 1);
}
void add(int i, int x){
for(i++; i <= n; i += i & -i) c[i] += x;
}
int sum(int i){
int s = 0;
for(i++, i = min(i, n); i > 0; i -= i & -i) s += c[i];
return s;
}
int range(int l, int r){
return sum(r) - sum(l - 1);
}
}t1[N], t2[N];
namespace LCA{
int sz[N]

本文介绍了点分树(Dynamic Point Decomposition,DPD),一种基于点分治的树上数据结构。点分树常用于处理树上的路径相关问题,其子树对应原树的连通块,树高为O(logn)。文章详细阐述了点分树的构造、性质,以及如何利用点分树解决路径统计和点对数量的问题。此外,还提供了点分树在动态点分治中的应用,包括求解路径上的点权和、树上点权范围查询等,并附带了多个例题解析和代码实现。
最低0.47元/天 解锁文章
751

被折叠的 条评论
为什么被折叠?



