树分治(三)——点分治题目选讲
点分治题目选讲
CF711C Bear and Tree Jumps Present 5
尽管这题可以有 \(O(nk^2)\) 的 dp 做法,但是仅限于边权为 \(1\) 的情况。
而点分治可以处理边权任意的情形!考虑当前根为 \(\text{rt}\) 的时候怎么算。
对每个 \(u\) 处理出来 \(\text{rt}\to u\) 路径上的边权和,记作 \(d_u\),那么要求的就是:
其中 \(b_u\) 表示 \(u\) 是从哪个子树过来的。我们要能快速算出这个东西就行了。
注意到 \(k\) 很小,考虑将 \(d_u\) 按照 \(\bmod k\) 的余数分类。
由于 \(d_u+d_v=k\lfloor d_u/k\rfloor+(d_u\bmod k)+k\lfloor d_v/k\rfloor +(d_v\bmod k)\),故
前一项可以直接算,考虑后一项怎么算。
设 \(f_i\) 为当前子树内满足 \(d_u\bmod k=i\) 的 \(u\) 的个数,\(g_i\) 为前面子树内 \(\bmod k=i\) 的个数,那么后一项的和就是
维护 \(f,g\) 以及 \(f\) 的前缀和即可。\(O(nk\log n)\)。AC Code
但说实话这题其实挺没意思的,,实际上我们可以利用长链剖分在 \(O(n)\) 时间内对每个 \(x\) 预处理出来长度为 \(x\) 的路径数 \(c_x\),然后答案就是 \(\sum c_i\lfloor i/k\rfloor\)。
点分治也可以算这个 \(c\) 数组,只不过麻烦一点,我目前只会 \(O(n\log ^2n)\) 的点分治+NTT >_<
CF833D Red-Black Cobweb Future 7.0
考虑点分治,设当前根为 \(r\),对于一个点 \(u\) 我们算出来 \(f_u,g_u\) 表示 \(r\to u\) 路径上黑边白边的个数。
那么一条路径合法当且仅当
变形得到 \(2f_u-g_u\le g_v-2f_v,f_u-2g_u\le 2g_v-f_v\)。貌似是一个二维数点的形式,复杂度 \(O(n\log ^3n)\)?
其实可以容斥:我们算出来比例 \(<1/2\) 与比例 \(>2\) 的路径乘积,然后容斥一下即可。这样就变成一维数点了,复杂度 \(O(n\log ^2n)\)。AC Code
CF715C Digit Tree Present 7.0
考虑点分治。对于当前根 \(\text{rt}\),对每个 \(u\) 预处理出来 \(\text{rt}\to u\) 路径上边权写下来之后 \(\bmod M\) 的值 \(f_u\),\(u\to \text{rt}\) 路径上边权写下来后 \(\bmod M\) 的值 \(g_u\) 以及深度 \(d_u\)。其中 \(d_\text{rt}=0\)。
那么两条路径拼一起的时候就是 \(u\to \text{rt}\to v\),也就是要求 \(g_u+f_v\times 10^{d_u}\equiv 0(\bmod M)\)。
也就是 \(f_v\equiv -g_u\times 10^{-d_u}(\bmod M)\)。由于 \(\gcd(M,10)=1\),\(10^{-1}\) 在 \(\bmod M\) 意义下确实存在。
那么就可以维护一个 map
然后直接算啦
时间复杂度 \(O(n\log n\log M)\),\(\log M\) 来自 map
。AC Code
ps:这题 \(M\) 不一定是质数,求逆元不能直接算 \(10^{M-2}\bmod M\) !!!!我因为这个调了四天!!
ABC248G GCD cost on the tree Future 7.0
考虑点分治,对于当前根 \(\text{root}\),设 \(g_u\) 为 \(\text{root}\to u\) 路径上点权 \(\gcd\),\(f_u\) 为 \(u\) 的深度。
依次处理 \(\text{root}\) 的每一棵子树,并维护一个集合 \(S\):
- 对于当前子树内的每个点 \(u\),算出 \(f_u\) 与 \(g_u\),并将 \(\sum_{v\in S}(f_u+f_v)\gcd(g_u,g_v)\) 累计进答案。
- 将每个点 \(u\) 插入进集合内。
现在只需要考虑如何维护集合 \(S\)。
简化一下问题,我们需要维护一个集合 \(S\) ,支持加入一个数,以及对于给定的数 \(y\),查询
注意到
预处理 \(\varphi\),并维护每个数在 \(S\) 中的倍数个数即可。
那么这道题也就相当于带了个权,把贡献拆成两部分就行了。
更具体地,我们要查询的东西相当于
前面一部分就是带了个权,后面一部分就是不带权的情况乘上 \(f_y\)。本质上没有什么变化。
时间复杂度:我们每次加入与删除的复杂度都是 \(O(d(y))\),其中 \(d(x)\) 表示 \(x\) 的约数个数。
随便写个程序验证一下就可以发现 \(\max_{i\in[1,10^5]}d(i)=128\),所以复杂度并不会太高。
总的复杂度为 \(O(nd(V)\log n+V\log V)\),其中 \(V=10^5\) 为值域。\(V\log V\) 来自预处理约数集合。
由于 \(d(V)\) 在大多数情况下只有 \(O(\log V)\),根本卡不满 \(128\),所以常数很小,可以看作 \(O(n\log ^2n)\)。AC Code
Luogu3714 BJOI2017 树的难题 Present 7.0
点分治,设当前分治中心为 \(r\),那么对于两条路径拼起来的时候,有两种情况:
- 两条路径最上面的颜色不同,那么直接把路径边权加起来就好了。
- 两条路径最上面的边的颜色相同,相当于把路径边权加起来再减掉一个这个颜色的权值。
分别用线段树维护区间 max 单点修改就行了,复杂度 \(O(n\log ^2n)\)。AC Code
CF150E Freezing with Style Future 8.0
给定一颗带边权的树,求一条边数在 \([L,R]\) 之间的路径,并使得路径上边权的中位数最大。输出一条可行路径的两个端点。
点数不超过 \(10^5\),边权 \(\le 10^9\)。这里,一个长为 \(k\) 的序列 \(a_0,a_1,\cdots,a_{k-1}\) 的中位数为 \(a_{\lfloor k/2\rfloor}\),不论 \(k\) 的奇偶。
首先二分。二分一个值 \(m\),那么一个序列中的中位数 \(\ge m\),就相当于这个序列中至少一半的数都 \(\ge m\)。
因此把 \(\ge m\) 的边权设为 \(1\),\(<m\) 的边权设为 \(-1\),求出此时树上边权和最大的路径,判断一下是否 \(\ge 0\) 即可。
现在的问题就是:求出树上边数在 \([L,R]\) 之间的路径,使得路径上边权和最大。
点分治。设当前处理到了根 \(\text{root}\) 的第 \(i\) 个子树。我们定义:
- \(p_x\):在前 \(i-1\) 个子树内所有与 \(\text{root}\) 距离为 \(x\) 的节点中,路径 \(\text{root}\to x\) 上边权和的最大值。
- \(q_x\):当前子树内所有与 \(\text{root}\) 距离为 \(x\) 的节点中,路径 \(\text{root}\to x\) 上边权和的最大值。
那么写一下我们要求出来的东西就是:
枚举 \(y\),我们要求的就是 \(p\) 在 \([L-y,R-y]\) 上的最小值。看上去可以直接开一个线段树求区间最小值。
分析一波复杂度:点分治每一层一个 \(O(n\log n)\),一共是 \(O(n\log ^2n)\)。再乘上二分就得到了一个很 naive 的 \(O(n\log ^2n\log V)\) 的 3log 做法,而且常数巨大。。。
显然过不去......考虑优化一下。
注意到我们枚举 \(y\) 的时候如果按照递增顺序,那么 \([L-y,R-y]\) 这个区间就在不断地从右至左滑动。
因此可以考虑用一个单调队列来维护滑动窗口最小值,这样点分治每一层就是 \(O(n)\) 了......?
然而并不是。我们处理到第 \(i\) 棵子树时,设前 \(i-1\) 棵子树内最深子节点深度为 \(\text{Maxdep}\),那么处理这一棵子树就需要 \(O(\text{Maxdep})\) 的时间。
举个例子,如果第一个子树深度达到 \(O(n)\),剩下的都是 \(1\),点分治一层的时间复杂度就成了 \(O(n^2)\)。看上去这个方法不行了。
其实解决方法很简单:还是以刚才那个例子来说,如果我们先处理那些深度浅的子树,那么显然就不会有这种问题。
因此可以考虑把所有子树按最大深度排序,然后从小到大依次处理。这里子树的深度指子树内最深节点的深度。
这样一来,由于我们是按深度从小到大处理的,设当前子树深度为 \(\text{dep}\),那么前 \(i-1\) 个子树中的 \(\text{Maxdep}\) 必然不会超过 \(\text{dep}\)。
关于排序的复杂度,注意到我们排序的点数实际上是根节点的子节点个数,这个东西的和是 \(O(n)\) 的,所以排序的总复杂度是 \(O(n\log n)\)。
因此,总的复杂度就是 \(O\big(\sum \text{dep}\big)=O(n)\) 了。总的时间复杂度为 \(O(n\log n\log V)\)。
注意到每次点分治的时候树的结构不变,因此可以提前预处理出来我们对这些点的访问顺序,然后每次二分的时候直接 for
循环,就省掉了递归的常数和找重心的时间。
代码:写挂了,留个坑
UPD:用长链剖分过了 <>
LuoguP4292 WC2010 重建计划 Future 8.0
给定一棵带边权的树,求一条边数在 \([L,U]\) 之间的路径,并使得路径上边权的平均数最大。
点数 \(10^5\),边权 \(10^6\)。
和上一题基本一样。。
二分答案 \(m\),将所有边权都减去 \(m\),就相当于找一条边权和非负的路径。直接做就行了。
Luogu2664 树上游戏 Future 8.1
考虑点分治,设当前根为 \(r\)。
对于一个点 \(u\),考虑把颜色数之和拆成两部分,一部分是在 \(r\to u\) 路径上出现过的,一部分是没有出现过的。
对于在 \(r\to u\) 路径上出现过的颜色,其贡献即为 所有点的个数 减去 \(u\) 所在子树的节点个数;对于没有在 \(r\to u\) 路径上出现过的颜色 \(c\),我们维护 \(f_i\) 表示颜色 \(i\) 出现在了多少条路径上,贡献就是 \(f_c\)。
这些东西都可以在 dfs 中顺便维护掉,于是这题就做完了。复杂度 \(O(n\log n)\)。AC Code