树分治

点分治

应用

  1. 树上所有路径统计
  2. 树上每个节点做根,信息统计。

注意:需要在求出重心后重新统计子树大小,别写假了。

还有就是一般点分治用于子树内 \(O(size)\) 信息的合并,如果子树内只贡献 \(O(1)\) 个信息,那么可以直接将信息放到状态中作树形 dp。参考 P2634 [国家集训队] 聪聪可可

P3085 [USACO13OPEN] Yin and Yang G

将两种颜色边权分别设置为 \(-1\)\(1\),转化为三段路径和为 \(0\)。如果统计中间的那个点和另一个端点的话需要用到乘法原理,有些麻烦而且无法排除同一端点不同中点的情况。

可以对于端点分类:祖先有可以做中点的点和祖先没有可以做中点的点,然后分来统计即可。

P4886 快递员

思路新奇的题目,如果枚举所有点做根,然后统计是 \(O(n^2)\) 的。

可是我们发现其实可以缩减计算量,假设当前以 \(c\) 为根,存在一个组 \((u_i,v_i)\) 使得两者是最长路径且经过 \(c\),或者存在至少两组 \((u_i,v_i)\) 使得 \(u_i\)\(v_i\) 在同一子树内,且两组点对不同子树。这样子就无法缩减答案了,否则进入那颗包含最大 \((u_i,v_i)\) 的子树。

借鉴点分治思想,每次选取子树内重心去跳,复杂度是 \(O(n\log n)\)

P2664 树上游戏

路径信息统计,考虑点分治。

由于颜色种类数是求并,很难直接做。

所以统计子树信息,然后放到一起求并是不现实的,这里不能按照普通点分治的统计思路。是一种全新的处理方法。

对于当前分治中心的答案贡献求解很简单,就是 dfs 一遍即可,不再赘述。

考虑一条路径 \(x-rt-y\),对于 \(y\) 的贡献。如果 \(c_u\)\(u-rt\) 中第一次出现,那么 \(c_u\) 可以做出贡献,具体来说 \(u\) 子树内的所有点都可以作为 \(x\),对于 \(rt\) 的其他子树内的点(作为 \(y\))做出贡献 \(sz_u\)。这么做就是完成了 \(x-rt\) 对于 \(y\) 的贡献,还需要完成 \(rt-y\) 对于 \(y\) 的贡献,还是在第一次出现的时候统计,累加的大小就是对于当前分治中心其他所有子树的大小(它们都可以作为 \(x\)),但是 \(x-rt\) 的贡献会和 \(rt-y\) 的贡献重复。于是在统计 \(rt-y\) 路径对 \(y\) 的贡献的时候,我们还需要时刻累加对应颜色外部 \(\sum [x\to rt 的贡献]\),减去即可。

有一个细节就是 \(rt\) 对于其他点产生贡献的影响,这个自己讨论一下就行了。

P4183 [USACO18JAN] Cow at Large P

设点的深度和到叶子节点的距离分别是 \(dep_i\)\(g_i\),一个点可以封锁的条件就是 \(dep_i \ge g_i\),可是我们要求封锁点尽量少,也就希望这个点尽可能往上,等价于儿子满足条件但父亲不满足这个条件。如果单纯是条件一的话可以用换根 dp 来求,可是每个点的父亲无法再换根,这就很难做。

观察一下我们会发现这里其实是一个子树产生 "1" 的贡献,这里有一个公式就是子树内 \(\sum(2-deg_i)=1\),可以发现子树内每个点都满足 \(dep_i\ge g_i\) 于是我们只需要对于每个满足要求的点 \(u\) 产生 \(2-deg_u\) 的贡献即可。点分治统计点对 \((u,v)\) 的贡献。于是对于任意根节点 \(r\),求 \(\sum\limits_{d_{r,u\ge g_r(u)}}(2-deg_u)\)

点分治可以统计所有点对之间的贡献,这也等价于每个点作为根的时候其他点的贡献。于是本题可以用点分治求解。

用换根 dp 求出每个点到叶子的最短距离 \(len_u\),然后以 \(rt\) 为根的分治中每个点深度为 \(d_u\)。那么 \(v\) 能对 \(u\) 产生贡献,必须满足 \(d_u\ge -d_v+len_v\)。这个容易通过排序加双指针解决。

  • 注意一下,对于那个度数求和公式的使用,首先是无向树,其次注意是“子树”,所以子树的根向上还有一个度也要算。

CF833D Red-Black Cobweb

对于边占比的信息不太好直接合并出来,于是我们考虑维护 \(a-kb\) 形式的信息使得合并之后可以快速查看是否满足比例约束。

具体来说我们设黑边个数为 \(a\),白边个数为 \(b\),那么我们维护四个信息 \((A,B,C,D)=(a-2b,2b-a,b-2a,2a-b)\)

于是 \((u,v)\) 点对可以产生贡献必须满足,\(A_u\le B_v\)\(C_u\le D_v\)。这是一个二维数点的形式。

直接排序 \(+\) 树状数组统计即可。如果去除同一子树内贡献呢,我们对于当前分治重心下同一子树的信息单独跑一边上述计算减去贡献即可。但是这么做感觉有点问题,就同一点对会产生重复贡献,由于是乘法需要开根号,这就需要取模意义下的开根号也就是二次剩余有点麻烦。

为了防止点对产生重复贡献,我们可以赋一个顺序,也就是子树顺序,然后直接 CDQ 即可。

时间复杂度 \(O(n\log^3 n)\)

边分治

点分治产生多颗子树,但是有的时候信息难以合并,这个时候就要用到边分治这样只会产生两部分,方便操作。防止超时应该用多叉树转二叉树

BZOJ 2870. 最长道路tree

求树上的一条链使得 \(len\times \min a_i 最大\)

直接对于边 \(i\) 进行边分治即可,然后双指针扫描。

\[ans=\max((f_x+f_y+1+val_i) \times \min(g_x,g_y)) \]

点分治做法就是维护每个最小值的最长链长。

动态点分治

就是多次询问的要用到点分治结构的东西。

我们对于点分治的每层重心之间连边得到的树就是点分树。不用显示建树,只需要记录点分树上的 \(fa_u\) 即可,毕竟一般只需要用到暴力跳父亲节点统计答案。

这样子树高是 \(O(\log n)\) 级别的,我们可以暴力跳点分树上的父亲来求解答案,注意细节需要去掉父子之间重复的信息。

P6329 【模板】点分树 | 震波

直接对于每个点 \(u\) 维护自点分树子树内距离自己为 \(k\) 的点权和,记为 \(C_{0,u,k}\)

查询就是不断跳父亲累加答案。思考一下为什么是这样子,因为这本质是一个路径统计问题,我们需要统计 \(u-x\) 距离 \(\le k\)\(\sum a_x\),路径统计可以类似点分治的方法解决,但是多次询问复杂度过高。在我们保存了各层分治中心之后,可以发现所有 \(u-x\) 路径必定可以拆分为 \(u-anc-x\),其中 \(anc\)\(u\) 在点分树上的祖先,根据点分治统计的完全性,可以知道这个方法是正确的,可以这么拆。

下面还要解决两个问题,一个是动态修改,很好办,直接把 \(C\) 替换为树状数组即可。还有一个就是 \(u,fa_u\) 之间的统计重复。我们可以设 \(C_{1,u,k}\) 表示\(u\) 子树内和 \(fa_u\) 距离为 \(k\)\(\sum a_x\)。这样子在儿子减去的,会在父亲加回来。每次更新点权的时候就直接暴力跳父亲,更新一路上的 \(C_0\)\(C_1\)。每次查询的时候就直接一边加 \(C_0\) 的贡献,一边减去 \(C_1\) 的贡献。

其实从另一种视角看待 \(u\)\(fa_u\) 之间的去重,其实就是我们在正常点分治过程中 \(rt\) 的同一子树内需要去重。以 \(u\) 为分治中心的所有点,在 \(fa_u\) 的视角中就是同一个子树内的点,其贡献需要减去,这么看是不是又有另一种理解了。

我们使用 vector 来储存 \(C\),对于 \(C_{0,u}\) 直接开 \(C\) 的最大深度大小,对于 \(C_{1,u}\) 要开 \(fa_u\) 的最大深度大小。

空间复杂度 \(O(n\log n)\),时间复杂度 \(O(n\log^2 n)\)

P3676 小清新数据结构题

  • 树链剖分解法

我们可以先动态维护根为 \(1\) 的时候的答案。有一个很简单的技巧就是把修改变为增加,对于一个点的修改影响的是所有祖先的子树和,于是我们只需要支持快速链加,全局求和即可。\(\sum (s_i+x)^2=s_i^2+2\times s_i\times x+x^2=\sum s_i^2+2x\sum s_i+len\times x^2\)。树链剖分维护即可。

然后考虑换根到 \(u\),不妨设路径为 \(1=u_0-u_1-...-u_k=u\)。跟为 \(1\)\(u\) 的时候子树和分为 \(a_i\)\(b_i\)
答案为 \(ans_u=ans_1-\sum\limits_{i=0}^ka_i^2+\sum\limits_{i=0}^kb_i^2\)

又因为 \(a_{i+1}+b_i=a_0=b_k\)(这一步是 key point),可以得到

\[\sum\limits_{i=0}^kb_i^2=\sum\limits_{i=1}^k(a_0-a_i)^2+a_0^2 \]

拆开维护一波即可。

  • 点分树做法

考虑将根从 \(1\) 一步一步移动到 \(u\),发现每次移动后都只有两个值会发生变化,也就是 \(s_{rt}\to sum-s_i\) 还有 \(s_i \to s_{rt}\)

于是我们可以发现 \(\sum(sum-s_i)\times s_i\) 为定值。

又因为 \(\sum s_i^2\) 正是我们需要求解的值,所以我们只需要维护 \(sum \times \sum s_i\) 即可。

\(sum\) 很容易维护,现在的问题就在 \(\sum s_i=\sum a_i\times (dis_{i,x}+1)\) 上面,这是动态点分治的模板。

在修改点权的情况下,\(\sum(sum-s_i)s_i\) 是会变化的,我们需要快速求值。考虑一下这个式子的意义,其实就是沿着 \(fa_i \to i\) 这条边把整个树划分成两个部分,然后两个部分的点对乘积和。再对于所有划分求和。

\[\begin{aligned} \sum(sum-s_i)s_i &= \sum\limits_{i=1}^n\sum\limits_{j=i+1}^na_i\times a_j\times dis(i,j) \\&=\sum\limits_{i=1}^n a_i \sum\limits_{j=i+1}^n a_j\times dis(i,j) \end{aligned} \]

单点修改,变化量就是 \((y-a_x)\times\sum\limits_{i=1}^na_j\times dis(x,j)\),又因为以 \(x\) 为根的树中,\(\sum s_i=\sum\limits_{i=1}^na_i\times (dis(i,x)+1)\)。所以直接维护即可。

P3345 [ZJOI2015] 幻想乡战略游戏

首先带权重心的结论,每次往 \(2\times sum_u\ge W\) 的子树走即可。

  • 线段树解法

依据上述结论,直接线段树二分。以 dfs 序为下标,子树权值和 \(sum_u\) 为值,记录区间最大值。如果能往右边走就走向右边。

找到重心 \(x\) 之后,考虑如何求出答案。

\[\begin{aligned}ans&=\sum\limits_{u}dis(x,u)\times a_u \\&=\sum\limits_{u}(dep_x+dep_u-2\times dep_{\mathrm {lca}}) a_u \end{aligned} \]

前两个非常好维护,只需要维护 \(\sum a_i\)\(\sum a_i\times dep_i\) 即可。对于第三个做法很神奇,就是把 \(dep_{\mathrm {lca}}\) 再拆分细化成每个边权的贡贡献,对于每个点 \(i\),将 \(i-rt\) 路径上的每条边都加上 \(a_i\times edge_j\),然后查询 \(x-rt\) 路径和即可。用树剖维护。

时间复杂度 \(O(n\log^2 n)\)

  • 点分树解法

本题限制了 \(deg_i\le 20\),其实不限制的话也可以三度化之后再做,详见 LOJ6896. 幻想乡战略游戏 加强版

考虑如何找到重心,我们在点分树上从根出发,遍历所有儿子,看看谁的 \(2\times sum_v\ge W\),走向它即可。度数限制保证了遍历儿子复杂度的正确性,树高为 $\log $ 级别的保证了遍历点分树复杂度的正确性。

但是还有一个问题,如图黑边为正常树边,蓝边为点分树上的边。在正常树上行走是红点走向橙点,然后橙点再向下找一个儿子继续走。但是在点分树上行走,我们是直接从红点到了另外一个红点。对于新的红点,在原树上存在向上行走的决策,也就是可能重心在橙色点以及其图中左右部分中,所以我们是有可能从新红点到橙色点的(橙色点为红色点在点分树上的子孙),但是我们这个时候的 \(sum\) 信息就不对了,因为点分树上所有点记录的 \(sum\) 只是其点分树子树内的权值和,并不包括外面这些 \(W-sum_u\)(也就是旧红点以及其右边部分,对应的也是以新红点 \(u\) 为根,橙色部分的子树)。我们称橙色点为接入点,每次需要在点分树上对于接入点以及其点分树上的祖先节点进行一个 \(+W-sum_u\) 的操作,这样才能保证 \(sum\) 信息时刻正确。然后在每次寻找完重心之后,要撤销这些增加操作。

找到重心之后还需要求解答案,这个就很好维护了吧。记录一下自己点分树子树内的点权乘以边权之和,还有到父亲的点权乘以边权之和减去即可。

时间复杂度 \(O(nD\log n+n\log^2 n)\)

P11343 [KTSC 2023 R1] 出租车旅行

给定一颗 \(n\) 个节点的树,从 \(u\) 一步走到 \(v\) 的权值为 \(a_u\times dis(u,v)+b_u\)。求从 \(1\) 走到其他所有点所需的最小权值和。\(n\le 10^5\)

注意本篇题解包括上述描述中的 \(a,b\) 数组和题面中的相反,读者自行 \(\rm {swap}\) 一下即可。

感受一下这个过程,本来可以一步到位的,我们之所以选择一些途径点,可能是因为当前点的 \(a_u\) 太大了,我们需要走到一些 \(a_u\) 比较小的点上再出发(和 \(b_u\) 无关,因为从一个点出发,不管往哪里走都需要支付 \(b_u\))。于是可以得到一个结论,我们途径点的顺序必然是从 \(a_u\) 大的走到 \(a_u\) 小的点,当然最后的终点不一定满足这个条件。

于是考虑按照 \(a_u\) 从大到小加入每个点作为途径点的信息,对于每个点要从 \(a_u\) 大于它的点中求出到它的最小距离,也要把它加入后续点的选择中。直接做不好求,可以用点分治思想,如果经过 \(rt\),从 \(u\)\(v\) 的代价就是 \(a_u\times d_v+(a_ud_u+b_u)\),这是一个一次函数可以用李超线段树维护。这启发我们建立点分树,每个点维护其子树内的一次函数,每次暴力跳父亲求其最短路,然后暴力跳父亲插入该点信息。

注意每次求完 \(dis(1,u)\) 之后,需要 \(b_u\gets dis(1,u)\),因为最短路需要类加上之前的路径之和。

上面插入的是途径点的信息,最后需要每个点作为终点再查询一下就行了。

时间复杂度 \(O(n\log^2 n)\)

P2056 [ZJOI2007] 捉迷藏

点分序

posted @ 2024-11-23 12:35  Mirasycle  阅读(36)  评论(0)    收藏  举报