点分治学习笔记

点分治

点分治适合求解树上路径问题。

就是每次选取树的重心,然后把树上的路径分为经过重心和不经过重心的,经过重心的直接考虑,不经过重心递归子树去算。

最多划分 \(\log n\) 次,所以时间复杂度是比较优秀的。

模板代码:

inline void getrt(int x,int fa){
	sz[x] = 1;f[x] = 0;
	for(int i = h[x];i;i = ne[i]){
		int y = e[i];
		if(y==fa||vis[y])continue;
		getrt(y,x);sz[x]+=sz[y];
		ckmax(f[x],sz[y]);
	}
	ckmax(f[x],s-sz[x]);
	if(f[x]<f[rt])rt = x;
}
inline void getdis(int x,int fa){
	if(dis[x]<=1e7)rev[++top] = dis[x];
	for(int i = h[x];i;i = ne[i]){
		int y = e[i];
		if(y==fa||vis[y])continue;
		dis[y] = dis[x]+w[i];
		getdis(y,x);
	}
}
// 辅助函数 $\cdots$
inline void cacl(int x){
// 做点什么
}
inline void solve(int x){
	vis[x] = 1;cacl(x);
	for(int i = h[x];i;i = ne[i]){
		int y = e[i];
		if(vis[y])continue;
		s=f[rt=0]=sz[y];
		getrt(y,x);
		solve(rt);
	}
}

P3806 【模板】点分治 1

求树上是否存在长为 \(k\) 的路径。

经过重心的路径的端点显然不能在同一颗子树。
值域比较小。
考虑开个桶记录每个子树到重心的路径是否存在。
然后就可以 \(O(1)\) 判断当前子树是否与之前的某个子树组成路径。

P4178 Tree

求树上路径长小于或等于 \(k\) 的点对数。

发现对于到重心的路径排序后可以双指针,但是比较难识别是否在同一子树内。
然后容斥一下就好了。

具体来说,重心为 \(x\),就是要求 \(\sum_{u,v}[d(u,x)+d(x,v) \leq k]\),但是 \(u,v\) 不属于同一个子树内。

发现按 \(d(u,x)\) 从小到大枚举 \(u\),满足条件的 \(y\)\(d(x,y)\) 单调递减。
那么我们对于 \(d(a,x)\) 排序,枚举左指针,右指针单调不升,就可以双指针统计答案。

然后考虑不在同一子树的贡献。
要加入一个儿子 \(y\),先计算 \(y\) 子树内部的答案,然后减掉。

但是值域比较小,也可以用树状数组维护前缀和。

不考虑同一子树限制时,容易全局计算答案的,可以考虑容斥。
如果考虑子树限制是容易的,那么也可以直接做。

P5306 [COCI 2018/2019 #5] Transport

从一个点走到另一个点当前仅当任意时刻路径上收集到的点权和大于等于边权和。

求有多少条满足条件的简单路径。

把路径分为以重心结尾的和从重心出发的。

维护从重心出发的点权和 \(d_x\) 和边权和 \(w_x\)

对于点 \(x\) 出发的,以重心结尾的路径,记路径上任意一点为 \(y\),则合法等价于对于所有 \(y\),均有 \(d_x-d_y \geq w_x-w_y\),即 \(d_x-w_x \geq d_y-w_y\)

维护 \(d_x-w_x\) 的前缀最小值,然后就可以判断是否合法。

对于从重心出发的,以点 \(x\) 结尾的路径,记路径上任意一点为 \(y\),某条路径到重心的剩余点权为 \(x\),则合法等价于 \(x+d_y \geq w_y\),即 \(x \geq w_y-d_y\)

维护 \(w_x-d_x\) 的前缀最小值。

单纯从重心出发的路径是好考虑的。

考虑合并路径。

发现我们可以对 \(x\)\(w_x-d_x\) 排序,然后可以双指针。

然后子树内的容斥也是容易的。

P4183 [USACO18JAN] Cow at Large P

求出以每个节点为根的情况下,至少要在叶子节点部署多少农民,才能保证根节点的人不会到达某个叶子节点。

农民和根节点的人移动速度相同,二者相遇即阻止成功。

二者均采取最优策略。

发现农民一定会尽力往上跳,因为这样可以堵住更多的叶子节点。

那么我们直接考虑某个点能不能被农民跳到,发现肯定是取深度最浅的叶子节点往上跳最优。

那么能来得及阻隔的等价于 \(d(x,y) \leq d(x,rt)\),其中 \(x\) 是被农民跳到的阻隔点,\(y\) 是最优叶子节点,\(rt\) 是当前根节点。

只要取恰好能把叶子节点全部覆盖的阻隔点集即可。

然后考虑换根的情况。

发现如果把最优叶子节点直接取全局最优叶子节点是没有影响的,因为最优叶子节点是不会取根节点后的叶子节点的(显然不满足不等式)。

记一个点的最优叶子节点到该点的距离为 \(p_x\)

那也就是对于某些 \(x\) 满足 \(p_x \leq d(x,rt)\),它会贡献 \(1\) 的答案。

发现 \(x\) 的子树内的所有点肯定也满足这个条件。

那很难把 \(x\) 单独找出来。

考虑给每个点赋一个点权,使得子树和为 \(1\)

我们考虑让所有儿子的贡献加上父亲的贡献为 \(1\),即父亲与儿子的贡献相抵消。

叶子节点为 \(1\),父亲为 \(1-\text{儿子个数}\),那也就是 \(2-deg_x\)

所以对于以 \(rt\) 为根的答案相当于 \(\sum_{p_x \leq d(x,rt)} 2-deg_x\)

对于这种路径问题考虑点分治。

考虑对于每个 \(p_x\),有多少路径的终点能贡献到它上面。

发现相当于 \(p_x \leq d(x,rt)+d(rt,y)\)\(y\) 都会贡献答案,即 \(p_x-d(x,rt) \leq d(rt,y)\)

对于上面可以双指针加前缀和。

同一子树内的容斥即可。

树上游戏

定义 \(S(x,y)\)\(x,y\) 简单路径上的颜色种类数。

\(sum_i = \sum_{j=1}^{n}S(i,j)\)

先考虑以 \(1\) 为根,考虑每个颜色会贡献多少个终点,发现就是第一次出现的颜色会贡献它的子树大小。

然后然后考虑点分治。

记重心为 \(rt\),容易求出所有点对 \(rt\) 的贡献,现在考虑经过重心的路径对每个点的贡献。

具体来说,先 dfs 求出子树大小。

然后开一个桶记录元素出现次数,对于第一次出现的元素贡献它的子树大小。

考虑对于某个方向的子树 \(y\) 计算贡献。

先暴力 dfs 减掉 \(y\) 的贡献。

注意到点 \(x\) 肯定会贡献它的子树大小,但是不考虑子树的话它的子树大小也会减少,所以还要再减去 \(sz_y\)

对于每个新出现的颜色,它的贡献都会变成 \(rt\) 的子树大小(因为必须经过重心,所以不会蔓延出点 \(x\))。

所以还要记录每个颜色的贡献。

P2634 [国家集训队] 聪聪可可

求出所为 \(3\) 的倍数的路径数(包括起点与终点重合的情况)。

点分治,合并子树贡献开个桶。

P4149 [IOI 2011] Race

最小化长度等于 \(k\) 的路径边数。

\(k\) 值域比较小,开桶。

P3714 [BJOI2017] 树的难题

统计经过边数在 \(l\)\(r\) 之间的所有简单路径中,路径连续颜色段权值和的最大值。

还是点分治。

考虑将子树按最大深度排序。

然后按深度从小到大 dfs 子树。

从重心出发的路径是好处理的。

对于合并贡献,决策区间单调递增,然后直接单调队列。

这样复杂度是对的。

但是如果还要处理同色区间,那么我们按同色区间的最大深度排序,再在颜色内部按深度排序,然后维护两个单调队列,这样是对的了。

或者也可以线段树。

点分树

你把点分治过程每次选取的重心建成一颗树,就是点分树。

与点分治相比,它可以支持动态修改。

同时如果有多次询问,比起每次寻找重心,它的常数也小。

代码:

void getrt(int x,int fa){}
void solve(int x){
    //
    fa[rt] = x;
    solve(rt);
}
inline void cg(int x){
  // p
  // fa,s
}
inline int qy(int x){
  // p
  // fa-s
}

P6329 【模板】点分树 | 震波

求与 \(x\) 距离小于 \(k\) 的点的点权和。
支持修改点权。

先把点分树建出来,考虑点分树上 \(lca(x,y)\) 必然在 \(x,y\) 的路径上。
所以枚举 lca \(z\),记 \(s\)\(z\)\(x\) 方向上的子节点,等价于求 \(\sum_{z \in father(x)}\sum_{y \notin S(s),y \in S(z)}v_y[d(x,z)+d(z,y) \leq k]+\sum_{y \in S(x)}v_y[d(x,y) \leq k]\)
也就是说,对于查询 \(x\),我们需要知道 \(x\) 子树内距离小于等于 \(k\) 的点权贡献以及 \(x\) 的 lca 们的子树内(不包括 \(s\))距离小于等于 \(k-d(x,z)\) 的点权贡献。
由于支持单点修改,又要查询前缀和,可以维护树状数组。

对于每个节点,维护一个树状数组用来查询子树内距离前缀点权和。
然后考虑父节点的“不包括子树 \(s\)”限制。
对于每个节点,再维护一个树状数组用来减去 \(s\) 子树内对于父节点 \(z\) 的距离前缀点权和贡献。

发现如果边权为 \(1\),最大距离绝对不会大于节点在点分树上的子树大小。所以树状数组的下标不用离散化。

P2056 [ZJOI2007] 捉迷藏

选定树上的一些点,求选定点的最远距离。

会动态删除或者选择一些点。

考虑建点分树。

考虑每个 lca 能贡献的答案,发现要求 \(\max\{d(y,z)+d(z,x)\}\),其中 \(x,z\) 不在同一子树内。
如果维护每一颗子树对自己贡献的最大值的话,每个 lca 的答案相当于子树贡献最大值和次大值的和。

然后更改一个点时,会更改对自己和所有 lca 的距离贡献。
所以对于每个节点,需要维护子树内所有点对父节点贡献的最大值,子节点贡献的最大值和次大值,全局答案的最大值。
支持修改。

所以对于每个节点子树内所有点对于父节点贡献维护线段树最大值,子节点贡献维护线段树最大值和次大值,全局答案维护线段树最大值。

预分配内存。

然后由于巨大的常数 \(T\) 了最后一个点。

然后发现支持修改先当于支持插入和删除,可以用两个大根堆维护该操作,一个是插入堆,一个是删除堆,当二者堆顶都相等时,就都弹掉。

常数就小了,代码也好写了。

P3345 [ZJOI2015] 幻想乡战略游戏

支持修改点权。

维护带权重心。

考虑二分,每次选取一个方向的重心,然后选择所有方向中答案更小的那个方向的重心。

然而如何计算答案。
相当于要计算 \(\sum_{i=1}^{n}d(x,i)v_i\)
考虑在点分树上求这个东西。

相当于 \(\sum_{y \in S(x)}d(y,x)v_y+\sum_{z \in lca_x,y \in S(z),y \notin S(s)}dis(y,z)v_y+dis(x,z)v_y\)

然后维护子树内的带距离点权和以及点权和。
还有该子树内对父节点的带距离点权和。

这样就可以 \(O(\log n)\) 查询了。

然后就可以得到 \(O(n (\log n)^2)\) 的做法。

P3676 小清新数据结构题

求以某点为根时所有子树点权和的平方和,支持单点修改点权。

考虑两点权什么时候会以相乘的形式贡献一次。

发现相当于求出以 \(i\) 为根时 \(\sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_jdep_{lca_{i,j}}\)

那深度显然不好换根,试图将其转换成距离。

即有:\(dep_{lca_{i,j}} = \dfrac{d(i,rt)+d(j,rt)-d(i,j)}{2}+1\)

也就是要求以下三个东西:

\(f_1 = \sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_j\dfrac{d(i,rt)+d(j,rt)}{2}\)
\(f_2 = \sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_j\dfrac{d(i,j)}{2}\)
\(f_3 = \sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_j\)

答案就是 \(f_1-f_2+f_3\)

\(f_1 = \dfrac{1}{2}(\sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_jd(i,rt)+\sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_jd(j,rt))\)

然后发现左右两式本质相同,所以有 \(f_1 = \sum_{i=1}^{n}\sum_{j=1}^{n}v_iv_jd(j,rt) = \sum_{i=1}^{n}v_i\sum_{j=1}^{n}v_jd(j,rt)\)

\(f_2 = \dfrac{1}{2}\sum_{i=1}^{n}v_i\sum_{j=1}^{n}v_jd(i,j)\)

\(f_3 = \sum_{i=1}^{n}v_i\sum_{j=1}^{n}v_j\)

然后记总点权和为 \(sum\)\(g_i = \sum_{j=1}^{n}v_jd(i,j)\)

则有 \(f_1 = sum \cdot g_{rt}\)
\(f_2 = \dfrac{1}{2}\sum_{i=1}^{n}v_i \cdot g_i\)
\(f_3 = sum^2\)

然后 \(g_i\) 是可以在点分树上维护的。

考虑点权变动对 \(f_2\) 的影响,从原式上看 \(v_i\) 会贡献 \(2\) 次,所以我们只考虑 \(v_i\) 变动后的 \(g_i\) 给他贡献的值即可。

当然这题还可以换根 dp + 树剖。

P3241 [HNOI2015] 开店

求出点权在 \(l,r\) 范围内的点到指定点的距离和。

建立点分树。

对于每个点,维护子树内对其的距离贡献和对字数内对父亲贡献以及子树大小。

然后对于下标离散化,再维护贡献前缀和。

这些可以预分配内存。

posted @ 2025-11-19 11:41  rabbit_mygo  阅读(20)  评论(0)    收藏  举报