树分治小记

command_block

2020-04-09 14:20:10

Personal

最近你可能在本`Blog`看到许多这样的小记,这完全属于省选之前的游击战术,内容不保证严谨,也不保证大多数人能看懂。 如果省选过后有时间我会来仔细更新并且把这些文字去掉的。 可能你会发现没有代码,这是因为我懒得贴,~~而不是在口胡或者咕咕咕~~,等我有空了会回来补的。 少数题目可能没有评测之处,这我也无能为力。 ------------ 内容比较杂,大概有: - 点分治 - 一类按重心移动的(最优化)判定问题 - 边分治 (+边分树合并) - (动态)链分治 (长链剖分 / `dsu on tree` / 全局平衡二叉树) - 点/边分树(建模) - 动态 · 树分治 **有些**(带记录标记的)链接会直接拉到博客中的其他文章(更丰富的)讲解,并不是一句话题解,请留意。 # 点分治 用于解决一类树上路径问题。 **核心思想** : 每次钦定一个点,只统计经过该点的路径。然后各个子树之间便不再有贡献,可以分治。 在树上,只需要选取重心作为分治中心,则可以保证各个子任务的规模都不超过原问题的一半。分治的深度是严格$O(\log_2n)$的。 - 怎么找重心 : 求出每个点的**最大子树大小**,包括朝向根的一部分,可以使用整个联通块的大小来减去本子树大小来计算。 然后取个最大子树大小最小的点即可。 然后,就变成了只处理过根的路径。一般来讲考虑“深度”,分上下讨论就可以了。 - **例题** [P4149 [IOI2011]Race](https://www.luogu.com.cn/problem/P4149) 以当前分治中心为根,经过根的路径的长度可以转化为两个端点的深度。 我们要拼合出恰好为$k$的路径,由于$k$较小,可以直接开桶维护。 主要的难点在于,路径不能自交,也就是说,两个端点必须来自不同子树。 解决的方法类似树形`DP`,先查询完某棵子树,再将其加入桶中(或其他数据结构)。这样就能保证只有来自不同子树的无序对才有贡献。 可以每次枚举修改来清空桶。 子问题大小总和是$O(n\log n)$的,总复杂度与之相同。 [提交记录](https://www.luogu.com.cn/record/33188080) - [P4178 Tree](https://www.luogu.com.cn/problem/P4178) 观察到$k$仍然很小,使用树状数组维护前缀和即可,复杂度是$O(n\log^2n)$。 - [P5351 Ruri Loves Maschera](https://www.luogu.com.cn/problem/P5351) 选取分治中心之后,令$mx[u]$为$u$到根的边权$\max$。 一条路径$(u,v)$的贡献即为$\max(mx[u],mx[v])$。 这里我们略施小计,把有序的路径钦定为在$mx$值较大的一段贡献。注意答案最后乘个$2$。 现在要计算总贡献。分“是最大值”和“不是最大值”讨论即可。 对于半截路径$mx[u]$,只需要考虑$mx$不超过自己的路径。 贡献为 : 不超过$mx[u]$的路径数$\times mx[u]$。别漏了分治中心本身。 这里还需要受到边的条数的限制,就变成了一个二维偏序问题。 但二维偏序是静态的,我们无法像前文那样依次加入各个子树。 注意到,求和类问题可以支持减去贡献,我们在每个子树内单独跑一次把贡献减去即可。 复杂度$O(n\log^2n)$。[评测记录](https://www.luogu.com.cn/record/33190803) - [CF293E Close Vertices](https://www.luogu.com.cn/problem/CF293E) 相信做过上一题之后能看出来是个裸的点分治+二维偏序。复杂度$O(n\log^2n)$。 [评测记录](https://www.luogu.com.cn/record/33220024) - [P3060 [USACO12NOV]平衡树Balanced Trees](https://www.luogu.org/problem/P3060) 从重心计算若干个向上和向下的括号路径。 对于向上的路径,括号对消之后一定得到若干个 `(` 。若遇见无法对消的 `)` ,则不合法。 对于向下的路径,括号对消之后一定得到若干个 `)` 。若遇见无法对消的 `(` ,则不合法。 具体而言需要维护一个栈。 对于向上的路径,后来 `(` 的可以消去前面的 `)`。 对于向下的路径,后来 `)` 的可以消去前面的 `(`。 栈在要记录是否曾经对消,便于回退。 只有消完之后括号数量相同的半路径才能够匹配。 注意,题目中求的是括号序列的“最大深度”所以还要维护这个东西。 抽象成`+1-1`前缀和就容易维护了,注意还要考虑栈中未对消的括号。 由于路径长度是$O($分治块大小$)$的,开个桶维护一下就好了。 这是取$\max$,贡献不可减,桶是支持动态加入的,那就沿用依次加入各个子树的方法。 我们可以把向上的路径加入桶,而用向下的路径来查询。不过这样就变成了有序对,需要正反各做一次。 [评测记录](https://www.luogu.com.cn/record/33194554) - [P3714 [BJOI2017]树的难题](https://www.luogu.com.cn/problem/P3714) 需要保存每条路径端点的颜色,如果两条路径端点颜色相同,则会减去一次贡献(可能是负数)。 有路径长度的限制,仍然考虑线段树维护。 现在我们要同时考虑端点颜色和子树编号两个限制,似乎有点麻烦。但是能够发现,端点颜色是取决于子树编号的。 我们把所有路径按照端点颜色为序,一次加入一个颜色,这就计算完毕了不同颜色之间的贡献,而且不会在同一子树内重复。 然后,我们对于每一种颜色,再分别按照子树编号依次加入即可,此时需要减去一次贡献。 注意清空线段树,这里并不需要标记。还要注意单独考虑从根向下的路径。 这题比较卡常数,$O(n\log^2 n)$似乎并不是正解,但随手加几个最优性剪枝就飞过去了。 [评测记录](https://www.luogu.com.cn/record/33195444) - [P5306 [COCI2019] Transport](https://www.luogu.com.cn/problem/P5306) 仍然是分上下讨论。我们钦定向上的路径包含根。 记录所有向上能到达根的路径,最多余下多少油。 我们一边向下`dfs`,一边维护油量的“最深坑”,也就是负得多惨。 我们要保证一路的油量都大于等于$0$,开始时的油量就不低于“最深坑”。 所有向下的路径,抵达终点需要在初始时额外支付多少油。计算方法类似。 然后排序再双指针扫一下,由于是求和问题,可以采用容斥的办法。 [评测记录](https://www.luogu.com.cn/record/33195776) - [P4183 [USACO18JAN]Cow at Large P](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p4183-usaco18jancow-at-large-p) 先推一些性质,然后容斥,转化成偏序问题。多询问的离线回答技巧。 - [[DP记录]P2305 [NOI2014]购票](https://www.luogu.com.cn/blog/command-block/post-ji-lu-p2305-noi2014-gou-piao) 按重心划分的树上`CDQ`,优化一类按顺序转移的斜率优化`DP`问题。 # 按重心移动 - **例题** [P4886 快递员](https://www.luogu.com.cn/problem/P4886) **题意** : 给出若干个点对,要求选定一个点$c$。 点对$(u,v)$的花费定义为$dis(c,u)+dis(c,v)$,问如何选定$c$使得所有点对花费的最大值最小。 考虑先随意使某个点为$c$,求出各个点对的花费,此时为两个点深度的和。 找出花费最大的点对(们)。 如果$c$在点对间的简单路径上,即点对在不同子树内,答案不可能变小。 又或者多个最大值分布在不同的子树内,答案同样不能变小。 反之,$c$只有向着最大点对所在的子树移动,才可能让答案变小。 于是,我们排除了其余各个子树。这相当于只递归一边的点分治。 我们每次找出点对需要遍历整棵树,是$O(n)$的,只需要$O(\log n)$(分治树深度)次,总复杂度是$O(n\log n)$的。 注意,由于是跳跃移动,答案可能反而变劣,需要不断取$\min$。 还有`grt`中覆盖`vis`的情况需要先判掉`return`。 [评测记录](https://www.luogu.com.cn/record/33189740) - [[??记录]CF566C Logistical Questions](https://www.luogu.com.cn/blog/command-block/post-ji-lu-cf566c-logistical-questions) 利用凸函数的性质和导数,能够得出答案在根的那一边。 - [ 神奇的题目-安排.png ] **题意** : 给出一棵树,有边权。安排一个有序点集$P$使得$|P|=k$。保证$k$是偶数。 需要令$\sum\limits_{i=1}^kdis(P_i,P_{i+1\bmod k})$最大。 首先考虑如果给出一个点集,我们要怎么安排顺序使得答案最大。 能够发现是找到重心,答案就是各个点到重心距离的两倍。 考虑构造 : 在重心的不同子树之间穿插,使得路径总是经过重心。 由于最大子树不超过$k/2$(即便是散点集,边加了权,仍然有这个结论),一定能造出来。 如果任何一个路径不经过重心,答案都会变小。 然后考虑利用重心的性质。先猜测一个重心,在各个子树中贪心地选取深度前$k$大的点。 如果我们猜中了最优答案,没有一个子树选取的点数会超过$k/2$。 我们先要证明,这是答案最优的充要条件。结论 : 重心到点集距离和最小。 假设我们已经有一个符合如上要求的重心$v$,但另有最优解$u$在旁等候。我们可以令$u$中选定的点集连接到$v$上,这会使得距离和增大(因为$v$不是他们的重心),然而这和$v$是局部最优解矛盾。 如果很不幸,某个子树选取的点数超过了$k/2$,我们很有理由怀疑,更优的重心在其所指的方向。 我们可以将此时的点尽量穿插配对,然而在最大的子树内,仍然有一些点无法和外面配对。 如果我们让重心远离它们,这样它们会“变深”,身后的少数点会“变浅”。这样,反而会助长最大的子树的扩张。 所以,为了让没有一个子树选取的点数会超过$k/2$,我们必须向着最大的子树进发。 按照套路,每次向着子区域的重心移动,复杂度是$O(n\log^2n)$的。 注意,当$k$为奇数的时候,可能出现两个子树选满$(k-1)/2$个点,而最后一个点不知道选在哪边的情况,似乎是不太可做的。 # 边分治 点分治中,我们每次统计“只统计经过中心的路径” 边分治中,我们每次统计“只统计经过某条边的路径”。 这条边的选取也很讲究,需要选择两侧联通块尽量均匀的那一条。方法类似。 直接上的话,复杂度是不对的,给一个菊花图就卡掉了。 我们可以添加一些虚边虚点,使得整个图的度数不超过$3$,称之为三度化。这样子就能切得比较均匀了。 注意,这些新加入的东西不能影响答案,一般边权为$0$。如图: ![](https://cdn.luogu.com.cn/upload/image_hosting/rka1v1mi.png) 由于三度化和虚边虚点处理,边分治一般要比点分治难写。 边分治有什么好处呢?每次分治的时候,子问题只会被切成**两半**,这会带来一些好的性质。 建议先使用边分治写点分治的题来熟练,如 `P4149`。 先不写三度化体验一把`TLE`的快感。 边分治时,找到分治边后,需要将其删除,此时用邻接链表较为方便。 [评测记录](https://www.luogu.com.cn/record/33699737) ,常数并不大。 - **例题** [ 奇怪的题目.bmp ] **题意** : 给出一棵树,边有两种边权$A,B$。 要求挑选出一条路径,使得$A$权值和在不超过$[L,R]$内,且$B$权值的异或和最大。 先以任意一个点为根,预处理出从根到每个点的异或和$w[u]$。 由异或的自反性,两个端点的$w$值`xor`起来就是路径异或和。 考虑点分治,现在$A$权值和被转化成了两个深度和,这一维的限制变成了偏序。 我们发现,我们只能建立**静态**的可持久化`01Trie`来应付查询,但是这里的贡献不可减,无法容斥。 所以我们转而采用边分治。在边分治中,只有两个子树。 我们只需要在其中一个建立静态结构,然后从另一个出发查询即可。 事实上,点分治如每次合并(直接重建)两个最小的静态结构,并且处理之间的询问,复杂度仍然是正确的。边分治在比较经典的题目中没有多大优势。 - [[DS记录]P4220 [WC2018]通道](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p4220-wc2018-tong-dao) 边分治之后,点只有两种属性。 - [[DS记录]P4565 [CTSC2018]暴力写挂](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p4565-ctsc2018-pu-li-xie-gua) 边分树是二叉树,我们可以类似线段树合并那样写边分树合并。贡献可能在合并时能够顺便计算。 # 链分治 我对这东西没啥很好的理解,暂且随便一讲。 对于路径统计问题,还有另一种思路 : 钦定一个根变为有根树,并且枚举`LCA`。 一般来讲,我们会按照`dfs`的顺序来逐次访问各个节点,这样,我们就能够挑选某个儿子来继承信息。这或许能够改善复杂度。 先将这棵树按照某种规则进行链剖分,即为每个点选择一个“重儿子”,优先`dfs`并保留信息,并将其他轻儿子的信息加入。 在加入信息之前,我们计算新信息与旧信息之间的贡献,不难发现他们代表的路径的`LCA`一定是当前点。 能够注意到,我们必须维护支持动态加入的数据结构(或者二进制分组?)。 - 如果加入的复杂度和**子树大小**相关,这就是**重链剖分**。 选取子树大小最大的儿子作为重儿子。其余的轻儿子都不会超过整颗子树大小的一半。 复杂度是轻儿子子树大小的和,可以转化为每个点上方的轻边条数。 而每经过一条轻边,子树大小至少减半,所以轻边条数是$O(\log n)$的,总合并次数是$O(n\log n)$的。 其实就是`dsu on tree`,树上启发式合并。 - 如果加入的复杂度和**子树最大深度**相关,这就是**长链剖分**。 选取子树深度最大的儿子作为重儿子。 复杂度是轻儿子子树深度的和,可以转化为长链的长度和。 由于长链不交,总合并次数是$O(n)$的。十分优秀 顺便一说,在这里轻儿子子树大小的和是$O(n\sqrt{n})$的。 考虑越往上长链严格越长,而总长度不超过$n$,所以向上只有可能经过$O(\sqrt{n})$条不同的长链。 其实可以看做一次删去一条链,`dfs`的行为也是如此。 某些时候,这类方法也不止于路径统计问题。 - **例题①** [P4149 [IOI2011]Race](https://www.luogu.com.cn/problem/P4149) (**树上启发式合并**) 我们可以用平衡树维护深度和边数,区间取$\min$即可。 树上启发式合并维护,复杂度是$O(n\log^2n)$或$O(n\log n)$,本质上是在重链剖分。 其实直接写线段树合并会达到货真价实的$O(n\log n)$,但是似乎脱离了我们链分治的分析。 - **例题②** [CF208E Blood Cousins](https://www.luogu.com.cn/problem/CF208E) (**长链剖分**) 注意到允许离线,我们用栈+`dfs`就能够轻松求出某个点的$k$级祖先。 现在问题就变成了 : 某个点子树内,距离该点为$d$的点的个数。 我们维护$f[u][d]$表示点$u$子树内,距离该点为$d$的点的个数。 不难发现,状态量和子树最大深度相关,我们采用长链剖分维护这个$dp$。 轻儿子的继承直接加法,是$O(n)$的。 重儿子的继承是难点,需要巧妙的分配空间。善用指针。 如果$v$是$u$的重儿子,把$f[u][1...len[u]]$分配给$f[v][0...len[v]]$,这样直接传上来就符合定义。 其他的题目可能没这么幸运,可能要对这些信息批量操作。 [评测记录](https://www.luogu.com.cn/record/33228321) - **例题③** [CF375D Tree and Queries](https://www.luogu.com.cn/problem/CF375D) (**dsu on tree**) 考虑朴素的树上启发式合并,用数组维护每个颜色出现的次数,以及出现$\geq i$次的有几种颜色,不难做到$O(1)$增量。 但是数组需要的空间较大,且不和时间(子树大小)有关,我们不能为一路上的每一个重儿子都开一个数组。 考虑如何只采用一个全局数组来维护。 先计算轻儿子内部的答案,并将其对全局数组的影响清除。(只是完全清除而已,不一定需要贡献可减) 然后计算重儿子的答案,并将其信息保留并且继承。 然后再次计算轻儿子的影响,和(之间的)贡献。 这种算法被一些人称之为 `dsu on tree`。 复杂度仍然是轻儿子子树大小的和,$O(n\log n)$ [提交记录](https://www.luogu.com.cn/record/26499203) - 附送简单题 [CF600E Lomsat gelral](https://www.luogu.com.cn/problem/CF600E) - [U92408 树](https://www.luogu.com.cn/problem/U92408) **题意** : 给出一棵树,定义$F(S)$为连通点集$S$所需的边数。求$\sum\limits_{i=1}^n\sum\limits_{j=i}^nF([i,j])$。 可以对于每条边分别考虑贡献。 发现跨越某条边的区间数不容易计算,但是不跨越的区间数较为容易。我们只需要分别考虑子树内和子树外。 我们其实就是要维护一堆位置包含了多少个区间。 考虑`dsu on tree`子树内在加入,子树外在删除。 对于子树内的,我们可以采用并查集维护。对于子树外的,可以用类似`ODT`的办法。 重儿子继承比较简单。但是影响清除只需要把并查集指针恢复原样,`ODT`初始化为一整个大区间即可。 复杂度是$O(n\log^2n)$。 [提交记录] () ```cpp ``` - [[DS记录]P4292 [WC2010]重建计划](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p4292-wc2010-zhong-jian-ji-hua) 01分数规划后,转化成给定边数区间的树上最长链。采用长链剖分+线段树。 # 点/边分树 - **点分树** 每次分治的时候,让各个子树的分治中心连接到当前中心上。这棵树的高度是$O(\log n)$的。 不难发现,任意两个点 $x,y$ 的点分树上`LCA`一定在原树中,两点的简单路径上。 而且在$x,y$的公共祖先中,`LCA`是唯一满足这个性质的。 点分树不一定是二叉树。 - **边分树** 每次分治的时候,新建虚点代表一条边,让两个子树的分治中心连接到当前虚点上,如果子树只有一个点则直接连接。这棵树的高度也是$O(\log n)$的。 不难发现,任意两个点的点分树上`LCA`所代表的**边**,一定在原树中两点的简单路径上。 而且在$x,y$的公共祖先中,`LCA`所代表的边,是唯一满足这个性质的。 边分树一定是二叉树。 在这一节中,我们更多地用它们来建模。 - [[DS记录]CFgym101234D Forest Game](https://www.luogu.com.cn/blog/command-block/post-ji-lu-cfgym101234d-forest-game) 问随机点分治复杂度,优雅地利用了点分树的性质。最终转化为点分治+卷积。 - [ 神奇的题目-点分治.png ] **题意** : 给出一棵树,问点分治的方案数。两个方案不同当且仅当某一个连通块所选的点分治中心不同。 $n\leq 5000$。 这相当于点分树计数。我们需要设计一个树形`DP`,但是树上的某**边**对点分树的影响比较难以分析。 我们考虑断开一条边$u,v$之后,如何拆分点分树。 设边一侧的点为白点,另一侧为黑点。对于每个点向上找到大点分树上的第一个同色祖先为父亲。 性质 : 任意两点的点分树上`LCA`,一定在原树中两点的简单路径上。 根据联通块的性质,可以推导出 : 如果两个点同色,则原树路径上的点都同色。 这也就是说,点分树上两个点同色,则它们的`LCA`与其同色。这就能证明两种颜色的点都各自连成一棵树(而非森林)。 反过来考虑,如果我们连接了一条边$(u,v)$,如何合并点分树。也就是说考虑什么样的大点分树能够拆成这两个小的。 容易得到 $u,v$ 合并后必然在同一条直上直下的链上,除此之外的约束只有父子关系。 一个结论是 : 我们只需要考虑把 $u$ 到点分树根的路径和 $v$ 到根的路径按任意顺序归并。 为啥不能动旁边挂着的子树呢? 其实我们可以证明分裂时,$u$到点分树根的路径上挂的树都是纯色的。 考虑反证,如果颜色不纯,则会与其他的异色点呼应产生`LCA`,而这个`LCA`正是$u$到点分树根的路径上的点,矛盾。 经过上述一阵毒瘤的推导,我们现在只需要关注子树的根节点在点分树中的深度。 设$f[u][i]$为点$u$的子树建立点分树,$u$本身深度为$i$的方案数。 在把$u$与儿子$v$贡献合并的时候有: $f'[u][i]=\sum\limits_{j=1}^i\sum\limits_{k=i-j}^{siz[v]}f[u][j]*f[v][k]*\dbinom{i-1}{j-1}$ 组合数这里减一是因为钦定了第$i$位。注意,归并方案数和$k$无关。 对$f[v][k]$做一个后缀和,再加上子树大小剪枝,就可以做到$O(n^2)$了。 # 动态 · 树分治 特意把标题加个点,不要搞错了。 众所周知,猫树是序列上的点/边分树,那么某些东西猫树能(动态)做,点分治能做,就说不定能上点分树。下面可能点分和边分傻傻分不清。 点分树的祖先节点相当于把从 $u$ 出发的所有目的地划分成了$O(\log n)$个联通快,而且有必经点,所以方便处理。 动态链分治不知道该怎么更通俗地描述,因为这东西恐怕只存于树中。大概就是考虑路径所经过的最高重链,以及最高处是轻链的情况。 - **例题①** [P3241 [HNOI2015]开店](https://www.luogu.com.cn/problem/P3241) 没有修改,但是强制在线。应该是难度最低的点/边分树套数据结构了。 图已经三度化,考虑边分治。从$u$开始在边分树上向上跳,会经过$O(\log n)$条关建边。 在每条关建边处,查询**对面**年龄范围内的点的距离和,注意要加上点数$\times$自己到关键边的距离。 这可以排序+前缀和维护,需要二分来查找界限。 到关建边的距离(深度)可以分层来存储。善用指针代替`vector`分配你的内存。 复杂度为$O((n+q)\log^2n)$,空间复杂度是$O(n\log n)$。 [评测记录](https://www.luogu.com.cn/record/33268906) 注意到答案是求和的形式,可以容斥成对前缀的询问。 这就可以采用可持久化边分树来做,按照年龄依次加入修改得到前缀的状态即可。 具体来讲,需要维护已经存在的深度和,以及个数和,这只需要$O(1)$个变量存储就好了。 可持久化的时候,需要把到根的路径`path copy`,还要记录往哪边走,具体见代码。 复杂度为$O((n+q)\log n)$,似乎比可持久化树剖时空双优,但是仍然打不过。 [评测记录](https://www.luogu.com.cn/record/33323522) - [[DS记录]CF757G Can Bash Save the Day?](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-cf757g-can-bash-save-the-day) 在前一题中可持久化边分树的解法基础上,加入了修改。需要写三度化。 - **例题②** [P4115 Qtree4](https://www.luogu.com.cn/problem/P4115) **题意** : 维护带负权的点集最长直径,支持对点集插入和删除。 由于带负权,直径的经典结论不再生效。 ① **动态点分治** 按照点分树深度用$O(\log n)$个数组存下距离信息。 对每个分治中心,每颗子树维护一个可删堆。 对于各个子树的堆顶再维护一个可删堆,我们每次取出第一和第二即可。(若采用边分治可省去) 对于全局的答案,还要维护一个可删堆。 总复杂度是$O(n\log n+m\log^2n)$,空间是$O(n\log n)$。 ② **动态链分治** 我们从被修改的点向上更新,只会经过$O(\log n)$条重链。 考虑某条重链, $d[i]$是 $i$ 号点沿轻链下行的最长长度, $s[i]$ 是重链上两点的距离前缀和。 我们要在答案路径经过的**最高重链**将其捕捉,这相当于维护最大的 $[i<j]d[i]-s[i]+d[j]+s[j]$。 分别维护 $(d[i]-s[i])$ 和 $(d[j]+s[j])$ 的区间最大值,然后`pushup`时更新跨过中线的贡献即可。 向上更新的 $d$ 可以取用本条重链 $(d[j]+s[j])$ 的最大值。 只有对 $d$ 的单点更新,可以(每条重链分别)使用线段树维护。全局的路径再使用一个堆维护。 注意,某个点的 $d$ 可能有多种来源(多个轻儿子),所以不能直接赋值,需要用可删堆维护。 我们还需要考虑路径的最高点不是重链而是轻链的情况,这等价于在维护 $d$ 的堆里面取出最大值和次大值。 复杂度是$O(n\log n+m\log^2n)$,常数较小。[评测记录](https://www.luogu.com.cn/record/33588040) - [SP2939 QTREE5 - Query on a tree V](https://www.luogu.com.cn/problem/SP2939) 现在变成指一个点询问最近的白点,而不是整体问题。 考虑这个点所在的重链,对应着前缀查询 $d[i]-i$ 的最大值,或者后缀查询 $d[i]+i$ 的最大值。 注意,可能与本点的(其他)轻链组成答案,同样在可删堆里面搞搞。 维护 $d$ 是类似的。 [提交记录](https://www.luogu.com.cn/record/33591563) 点分治当然也能做,由于绕路一定不优,可以不处理重复的贡献,代码较短。[提交记录](https://www.luogu.com.cn/record/33592869) - **例题③** [P6329 【模板】点分树 | 震波](https://www.luogu.com.cn/problem/P6329) **题意** : 树上单点修改,每次查询到$u$的距离不超过$d$的点的权值和。 强制在线,$n,m\leq 10^5$ ,时限$\texttt{2s}$. 在没学点分树之前一直以为这东西不可做。 这个模板题并不是最简单的点分树,如果我还能找到更简单的就会更新。 - 退化为链时猫树咋做 对于每个分治区间,考虑跨越中点的点对的贡献。 如果 $u$ 不在分治区间内,无贡献。 如果 $u$ 在左/右边,会查询另一半的前/后缀和。然后在所在的一半分治下去。 修改只会改动 $O(\log n)$ 层的前/后缀和,树状数组就好了。 - 单组询问点分治咋做: 如果 $u$ 不在当前分治区域内,跳过。 如果 $u$ 在当前分治区域内,设 $u$ 到关键边的距离为 $k$。 查询**其他子树**到根距离不超过 $d-k$ 的部分的权值和,然后就变成子问题了。 - 多组询问点分树咋做: 对每个分治中心,维护每颗子树的以深度为下标的权值前缀和。 可以树状数组,大小只需要开到深度即可。总空间$O(n\log n)$. 询问时会在$O(\log n)$层的分治块求和。需要求整体的和再减去自己子树内部的贡献,如果写边分治则无需考虑。 每次单点修改时,会修改$O(\log n)$层的分治块,可以树状数组维护。 总复杂度是$O(n\log n+m\log^2n)$ [评测记录](https://www.luogu.com.cn/record/33291898) 链分治并不太能做,因为轻链只能传递少量信息(适合传统的单路径统计),而这个题需要以深度为序的较多信息。 - **全局平衡二叉树** 用于解决树链操作这一经典问题。 显然是可以直接树剖做的,但是复杂度为$O(n\log^2n)$。 `LCT`的复杂度是$O(n\log n)$,但是常数过大,实际效率并不会更优秀。 现在就要祭出上古魔法 : “全局平衡二叉树”。(可见《QTREE解法的一些研究》,2007) 树剖无疑比`LCT`更加轻便(指思想和分析)。轻重链的选择十分自然,每条重链都用完美平衡的静态结构来维护(虽然更多人的做法是写一个大结构)。可惜的是,复杂度是$O(n\log^2n)$。 究竟是什么设计,让`Splay`胜过完美平衡的静态结构呢? 我们考虑学着`LCT`把轻边拎着的重链的维护结构,挂在真正的父亲上,而且认父不认子。 我们引用一张古老论文里的例图,顺便一睹全局平衡二叉树的真容: ![](https://cdn.luogu.com.cn/upload/image_hosting/cvcbtlez.png) (“二叉树”这个称呼似乎不那么精确,应该称“二叉树森林”或者“多叉树”,取决于算不算轻边) 若要链操作,而在一条重链上区间操作,从全局树上看,下一条待命的重链正好挂在上一次结束的点上。 我们惊奇的发现,这棵全局树的深度,正对应链操作的复杂度! 而树剖的结构只是局部平衡,连上轻边之后可能导致深度增加到$O(\log^2n)$。 (不过倘若不刻意卡,常数会很小。而另一个好处是可以询问和操作子树。) 下一步是试图造出一棵深度为$O(\log n)$的全局树。 若要做到这一点,我们必须认识到 : 单条重链结构的点不止代表了自己,还代表着挂着的一大堆虚子树。 我们可以设$s_i$为重链上第$i$个点本身以及挂着的一堆东西的总大小。这些玩意必须按顺序组成一棵排序树。 我们将其排成一排,找到“重心”,也就是把左右两边分得尽量平均。然后将重心作为根,两侧递归建树。 我个人更习惯`Leafy`的结构,因为更便于拆分区间和可持久化。如果需要构建这样的结构,这里的“重心”就要变为类似边分治的“关建边”。 我们来证明复杂度 : 加权找重心是危险的,因为大块头往往会令人失算。 先不考虑重心及其连带着的部分,不难证明,剩下的两部分都总是小于$(\sum s_i)/2$的。 而重链的性质很好,被我们拎出来的重心挂着的轻儿子总和,同样不会超过整个子问题规模的一半。 这就证明了全局树的深度是$O(\log n)$的。我们构造时的复杂度不超过其每个点的子树大小和,为$O(n\log n)$。 这棵树是静态结构,常数较小,复杂度货真价实,也并不难实现。 - 附送练习题 : 1. [P4211 [LNOI2014]LCA](https://www.luogu.com.cn/problem/P4211) 非常适合的练手题,然而这个做法并没有多大的效率优势。[评测记录](https://www.luogu.com.cn/record/33298489) 2. [P4719 【模板】"动态 DP"&动态树分治](https://www.luogu.com.cn/problem/P4719) 结合`ddp`的理论,优化较为明显。[评测记录] () 实际上,大多数的操作都可以跳父亲无递归实现,这和点分树有点类似。 可能你发现了,这些操作都是到根的。实际上,不到根的路径操作(区间而非前缀)如噩梦般难写,如果遇到了还是弃疗写树剖吧…… 如果将同一个点上挂着的轻儿子,同样使用加权二叉树维护,似乎就能够做到子树操作(静态`Top tree?`)。 - [[DS记录]P3345 [ZJOI2015]幻想乡战略游戏](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p3345-zjoi2015-huan-xiang-xiang-zhan-lve-you-hu) 利用重心的一些(很多)性质,包括带权的和不带权的。 - [P3920 [WC2014]紫荆花之恋](https://www.luogu.com.cn/problem/P3920) **题意** : 给出一棵带权树,每个点有参数 $r[u]$ ,求 $r[u]+r[v]\geq dis(u,v)$ 的点对数目。 由于这样太简单了,还需要支持挂叶子。强制在线。 $n\leq 10^5$ ,时限$\texttt{12s}$。 真正的毒瘤 到 来 了 。 先不考虑挂叶子,进行普通点分治。 每次选定分治中心之后,答案是 $dep[u]+dep[v]\leq r[u]+r[v]$ 的点对数目。 可以变成 $dep[u]-r[u]\leq r[v]-dep[v]$。使用平衡树维护。 挂叶子的时候,我们可以暂且在点分树中,先将其挂在父亲的下面。 沿着点分树一路向上更新,会有 $O(h)$ 个计算路径长度(需要倍增),以及平衡树插入操作。 然后需要查询贡献。在每个分治中心处,需要查询其他子树的贡献。 这可以维护一个大平衡树,再给每颗子树维护一个小平衡树,查询两次减去即可。 但是,随着叶子越挂越多,点分树的深度 $h$ 也会变深,将会导致复杂度退化。 一个憨逼的想法是 : 模仿替罪羊树,重构不平衡的局部点分树。 重构的时候其实就是局部点分治,由于需要 `sort` 建立平衡树,复杂度是两个 $\log$ 的。 可以考虑对较大的数组基数排序。具体地讲,在 $O(n\log n)$ 和 $O(n\log_nS)$ 之间权衡,这样可以把排序的 $\log n$ 摊平成 $\sqrt{\log S}$ ,而且不会增大常数。 传统替罪羊的重构比修改低一个 $\log$ ,此时 $\alpha=0.75$ 才比较快。 本题中而重构复杂度较大,则需要把 $\alpha$ 调大一点 (如 $\alpha=0.8$ )。 > 仔细分析,将替罪羊在最差情况下理解成类 $a$ 进制分组,则重构的复杂度是 `lowbit` 求和,为 $O(n\log_a n)$ ,修改的复杂度为 $O(an\log_an)$。 > 本题中,这样所能得到的最优复杂度为 $O(n\log^2n\frac{\sqrt{\log S}}{\log\log n})$ (毒瘤,反正比 $O(n\log^3n)$ 要快) 平衡树一定要比较快,不然可能被卡常数。需要写个内存垃圾桶来回收平衡树节点。 局部点分治的时候,可以先跳父亲来封边界。 注意某个联通快块重构之后,父亲还是得更新。 ```cpp #include<algorithm> #include<cstring> #include<cstdio> #include<vector> #define ll long long #define pb push_back #define INF 1000000000 #define MaxN 100500 using namespace std; int read(){ int X=0;char ch=0; while(ch<48||ch>57)ch=getchar(); while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar(); return X; } struct BST_Node {int l,r,x,c;}a[MaxN*66]; int rub[MaxN*66],rut,tn2; int cre(){ if (!rut)return ++tn2; int pos=rub[rut--]; a[pos].l=a[pos].r=0; return pos; } int sx[MaxN],st2[255],tot2; struct BST { #define st st2 #define tot tot2 int rt; BST(){a[rt=cre()].x=INF;a[rt].c=1;} inline void up(int u){ a[u].x=a[a[u].r].x; a[u].c=a[a[u].l].c+a[a[u].r].c; } int find(int x) { int u=rt;tot=0; while(1){ if (a[u].l==a[u].r)return u; st[++tot]=u; if (x<=a[a[u].l].x)u=a[u].l; else u=a[u].r; } } void del(int u){ rub[++rut]=u; if (a[u].l==a[u].r)return ; del(a[u].l);del(a[u].r); } void clr(){del(rt);rt=0;} void pia(int u) { if (a[u].l==a[u].r){ sx[++tot]=a[u].x; rub[++rut]=u;return ; }pia(a[u].l);pia(a[u].r); rub[++rut]=u; } void build(int l,int r,int &u) { u=cre(); if (l==r){ a[u].x=sx[l];a[u].c=1; return ; }int mid=(l+r)>>1; build(l,mid,a[u].l); build(mid+1,r,a[u].r); up(u); } #define chk(u) a[u].c*31 < 40*max(a[a[u].l].c,a[a[u].r].c) void add(int x) { int u=find(x),nu=cre(),nv=cre(); a[nu]=a[u];a[u].r=nu; a[a[u].l=nv].x=x;a[nv].c=1; up(u); int reb=0; for (int i=tot;i;i--){ up(st[i]); if (chk(st[i]))reb=st[i]; }if (reb){ tot=0;pia(reb); build(1,tot,reb); } } int rnk(int x) { int u=rt,ans=0; while(1){ if (a[u].l==a[u].r)return ans; if (x>=a[a[u].l].x) {ans+=a[a[u].l].c;u=a[u].r;} else u=a[u].l; } } #undef st #undef tot }T[MaxN],T2[MaxN]; vector<int> g[MaxN],l[MaxN],tv[MaxN]; void adl(int f,int t,int len){ g[f].pb(t);l[f].pb(len);tv[f].pb(0); g[t].pb(f);l[t].pb(len);tv[t].pb(0); } int st[MaxN],tn,ms[MaxN],siz[MaxN], tef,ef,vis[MaxN]; void pfs(int u) { siz[st[++tn]=u]=1; vis[u]=ef;ms[u]=0; for (int i=0,v;i<g[u].size();i++) if (vis[v=g[u][i]]<ef){ pfs(v); siz[u]+=siz[v]; ms[u]=max(ms[u],siz[v]); } } int grt(int u) { tn=0;ef++;pfs(u); int rt=0; for (int i=1;i<=tn;i++){ ms[st[i]]=max(ms[st[i]],tn-siz[st[i]]); if (ms[st[i]]<ms[rt])rt=st[i]; }return rt; } struct PDC_Node {int f,c,d;}t[MaxN]; int r[MaxN],d[38][MaxN],*td,sd[MaxN],tot; void gdis(int u) { sd[++tot]=td[u]-r[u]; vis[u]=ef; for (int i=0,v;i<g[u].size();i++) if (vis[v=g[u][i]]<ef){ td[v]=td[u]+l[u][i]; gdis(v); } } void qs(int *s,int n) { if (n<=256){sort(s+1,s+n+1);return ;} static int o[260],g[MaxN]; for (register int i=1;i<=n;++i)s[i]+=INF; for (register int i=1;i<=n;++i)++o[(s[i]&255)+1]; for (register int i=1;i<=255;++i)o[i]+=o[i-1]; for (register int i=1;i<=n;++i)g[++o[s[i]&255]]=s[i]; memset(o,0,sizeof(o)); for (register int i=1;i<=n;++i)++o[((g[i]>>8)&255)+1]; for (register int i=1;i<=255;++i)o[i]+=o[i-1]; for (register int i=1;i<=n;++i)s[++o[(g[i]>>8)&255]]=g[i]; memset(o,0,sizeof(o)); for (register int i=1;i<=n;++i)++o[((s[i]>>16)&255)+1]; for (register int i=1;i<=255;++i)o[i]+=o[i-1]; for (register int i=1;i<=n;++i)g[++o[(s[i]>>16)&255]]=s[i]; memset(o,0,sizeof(o)); for (register int i=1;i<=n;++i)++o[(g[i]>>24)+1]; for (register int i=1;i<=255;++i)o[i]+=o[i-1]; for (register int i=1;i<=n;++i)s[++o[g[i]>>24]]=g[i]; memset(o,0,sizeof(o)); for (register int i=1;i<=n;++i)s[i]-=INF; } void build(int u,int dep) { t[u].c=1; (td=d[t[u].d=dep])[u]=0; vis[u]=tef;tot=0; for (int i=0,v,sav,vrt;i<g[u].size();i++) if (vis[v=g[u][i]]<tef){ tv[u][i]=vrt=grt(v); sav=tot; td[v]=l[u][i]; ef++;gdis(v); T2[vrt].clr(); for (int j=sav+1;j<=tot;j++) sx[j-sav]=sd[j]; qs(sx,tot-sav);sx[tot-sav+1]=INF; T2[vrt].build(1,tot-sav+1,T2[vrt].rt); } sd[++tot]=-r[u]; T[u].clr(); for (int i=1;i<=tot;i++)sx[i]=sd[i]; qs(sx,tot);sx[tot+1]=INF; T[u].build(1,tot+1,T[u].rt); for (int i=0,v;i<g[u].size();i++) if (vis[v=g[u][i]]<tef){ t[v=tv[u][i]].f=u; build(v,dep+1); t[u].c+=t[v].c; } } int f[MaxN][18],dis[MaxN],dep[MaxN]; int lca(int x,int y) { int k=17; if (dep[x]>dep[y])swap(x,y); while(k--) while(dep[f[y][k]]>=dep[x]) y=f[y][k]; if (x==y)return x; k=17; while(k--) while(f[x][k]!=f[y][k]) {x=f[x][k];y=f[y][k];} return f[x][0]; } int dist(int x,int y) {return dis[x]+dis[y]-(dis[lca(x,y)]<<1);} int tn3; #define tn tn3 void find(int u) {tn=0;while(u){st[++tn]=u;u=t[u].f;}} ll ans; #define chk2(u) t[t[u].f].c*4 <= t[u].c*5 void add(int u) { find(u); for (int i=1;i<=tn;i++)t[st[i]].c++; int reb=0; for (int i=tn-1;i;i--) if (chk2(st[i])) {reb=st[i+1];break;} if (reb){ tef=ef+(t[reb].c<<2)+10; for (int i=tn;st[i]!=reb;i--)vis[st[i]]=tef; int rt=grt(reb); t[rt].f=t[reb].f;swap(T2[rt],T2[reb]); build(rt,t[reb].d); ef=tef; find(rt); for (int i=2,v,len;i<=tn;i++){ v=st[i]; len=(d[t[v].d][u]=dist(v,u))-r[u]; T[v].add(len);T2[st[i-1]].add(len); }find(u); }else { t[u].d=t[t[u].f].d+1; for (int i=2,v,len;i<=tn;i++){ v=st[i]; len=(d[t[v].d][u]=dist(v,u))-r[u]; T[v].add(len);T2[st[i-1]].add(len); }T[u].add(-r[u]); } for (int i=tn,v,len;i>1;i--){ len=r[u]-d[t[v=st[i]].d][u]; ans+=T[v].rnk(len)-T2[st[i-1]].rnk(len); } } #undef tn int n; int main() { read();vis[0]=INF;ms[0]=(n=read())+1; read();read();puts("0"); T[1].add(-(r[1]=read())); t[1].c=1;dep[1]=1; for (int i=2,u,c,len;i<=n;i++){ t[i].f=f[i][0]=u=read()^(ans%1000000000); adl(u,i,len=read()); dis[i]=dis[u]+len; dep[i]=dep[u]+1; r[i]=read(); for (int j=1;j<17;j++) f[i][j]=f[f[i][j-1]][j-1]; add(i); printf("%lld\n",ans); }return 0; } ```