点分治(不是奶龙写的)
下文可能会出现一些把点分治叫成淀粉质的情况。
事情都要从学长让我写一些神秘题开始说起,然后发现我不会点分治了,于是严肃加训。
基础
首先考虑分治是一个什么东西。就是每次找一个端点,然后计算跨端点的贡献。同时,选择的这些端点具有某些特殊性质,以保证递归层数不会太多,进而每层内可以遍历区间内所有元素,去计算一些信息。
不难发现,这样计算是不重不漏的(可能需要特判 \(l=r\) 的贡献,这个一定不能忘记)。
在树上,我们也可能会遇到一些这一类的问题,那么,我们能不能把这种策略搬到树上呢?
显然是可以的。下面我们引入一些例题。
P4178
计算点对数量这个东西,按照上面的策略来说,我们应该让它跨过已些东西计算,同时保证跨的这些点有一些良好性质。
于是,对于一个分治中心 \(u\),我们只去计算跨过 \(u\) 的点对的贡献。同时,为了防止出题人给我们一条链把我们创飞,我们需要每次选择子树内重心当作分治中心。
接下来考虑在每一层内把所有点待算点全都挖出来,然后按距离排序,夹逼双指针即可。这样是两只 \(\log\) 的,显然能过。
这里显然你不能选择自己和自己作为一个点对,所以不需要特判。
代码细节非常多,可以参考实现(虽然好像没 AC 看不到,但是扔剪贴板里可能被我哪天删了())。
P3806
仍然是板子题。
听说按照上道题的方法跑 \(m\) 次点分治会被卡(好像是因为常数比较大),于是把询问离线下来,每次弄完 \(O(m)\) 判断是否有询问合法了。
剩下的和上道题没区别,只是不需要双指针了而已。
P5563
你发现他让你求模意义下的最大值,我们先考虑原来的做法,把两段拼到一起。
你发现,直接找最大的可能是错的,因为你可能取模后变得更小,用一个更小的数但是加起来不被取模掉的可能更优。
但是,仔细想想发现,其实只要在过程中记得取模,那么两个数加起来取模后至多减掉一个 \(mod\)。
所以你直接找最大值算一次,再二分出最后一个不会取模掉的数算一次就行了。
具体实现可以用 multiset,先把所有加进去,然后临时把这个子树里的东西挖出来计算。
CF161D
作业,但是放个代码。
不基础
注意到点分治只能求静态问题的答案。那如果带修改怎么办呢?
显然有一个东西叫动态点分治(点分树)。点分树有一些很好的性质:
- 每个子树内的点在原树上都是一个连通块。
但是你怎么知道我被这个卡了一下午。
这是因为,其实点分树上每条边相当于把当前树弄成几个连通块,然后点分树每个子树就各自对应了一个连通块。
- 两点 \(u,v\) 在点分树上的 LCA 在 \(u,v\) 原树的路径上。
我们假设 LCA 为 \(p\)。你发现,如果 \(p\) 不在 \(u,v\) 路径上,那么意味着如果你从 \(p\) 把原树裂成好几份的话,\(u,v\) 在同一棵子树里,显然此时 \(u,v\) 在点分树上的 LCA 不会是 \(p\)。
- 点分树树高是 \(O(\log n)\) 量级的。
显然你往上跳一次子树大小至少乘 \(2\),所以至多跳 \(O(\log n)\) 的。
然后考虑一个事情,我们在树上统计点对相关东西时,经常会枚举它们的 LCA。若我们确定了其中一个点,则显然的一种暴力是暴力跳这个点的祖先钦定其为 LCA 并计算。
这里由于点分树的良好性质,暴力跳的时间复杂度是可接受的。
P6329
在线树上邻域数点。
下文的距离都指原树上两点的距离,子树都指点分树上的子树,祖先都指的是点分树上的祖先。
设 \(f_1(u,d)\) 表示点分树上 \(u\) 子树中距离 \(u\le d\) 的点的点权之和,\(f_2(u,d)\) 表示点分树上 \(u\) 子树中距离 \(fa_u\le d\) 的点的点权之和。
注意,这里并没有 \(f_1(u,d-1)=f_2(u,d)\) 的关系式,因为原树上的距离和点分树上的距离并不相等,甚至可能没有什么关系。
假设我们现在要求一对 \((x,k)\) 的答案,根据上面的东西,一个自然的想法是直接从 \(x\) 往上跳,然后宣称跳到的这个点就是我们要算的 \((x,y)\) 的 LCA。假设我们当前跳到了 \(p\)。显然,此时的贡献是 \(f_1(fa_p,k-\operatorname{dist}(fa_p,x))-f_2(p,k-\operatorname{dist}(fa_p,x))\)。这是因为你要减掉 LCA 在 \(fa_p\) 真子树里的已经算过的贡献。然后这里初始还有个贡献,这个很显然就不说了。
然后只有询问的情况就做完了(哦其实没完全做完,但是 \(\operatorname{dist}\) 的求法是简单的,用重剖随便单 \(\log\) 做),我们考虑怎么修改。
不难发现,修改你也可以从 \(x\) 一直往上蹦,同时你发现单点改和单点加没区别,所以直接对于跳到的点加上新权值减去旧权值的差即可。
至于维护这个 \(f_1,f_2\),直接用 vector 实现的树状数组就行,由于我们上面的性质,只需要把树状数组的大小初始化为点分树子树大小即可。然后你就做到了 \(O(n\log^2 n)\)。
P2056
相当于维护虚树的直径。
首先每个点仍然只计算以点分树上每个点为 LCA 的路径。于是你发现,修改一个点只会影响到它的祖先,所以你可以暴力跳修改。
但是类似上一题,你的 LCA 实际上有可能在子树内,然后就寄掉了。上一题因为你的信息是可减的所以直接减就行,但是最大值你显然减不了一点。
所以你不得不在每个点上开一个 multiset 去存东西。具体地 \(f_i\) 存储所有 \(i\) 子树内的点到 \(fa_i\) 的距离;\(s_i\) 为所有 \(i\) 子节点的 \(f_i\) 的最大值加上次大值。
于是答案是所有 \(s_i\) 的最大值。根据我们上面说的,一次修改只有 \(O(\log n)\) 个点,每次修改是 \(O(\log n)\) 的,所以最后是 \(O(q\log^2 n)\) 的。
听说直接用 multiset 会被卡常,用两个堆实现 multiset 就过了,手写堆应该也可以。
CF337D
习题,好做的,仍然扔个代码。

浙公网安备 33010602011771号