树分治(二)——边分治与动态点分治

边分治

和点分治类似,我们在树上取一条边,然后将路径分为「不经过这条边的路径」和「经过这条边的路径」,如图:

\((1,4)\) 将整棵树分为了两部分。

路径 \(3\to 2\to 8\) 不经过这条边,属于第一类路径;路径 \(2\to 1\to 4\to 6\) 经过这条边,属于第二类。

对于第一类路径递归下去计算,第二类路径和点分治类似。

每次选择两边 \(\text{size}\) 较大值尽可能小的边进行处理,递归深度就是 \(O(\log n)\)......吗?

实际上并非如此=_= 考虑一个菊花图,可以发现不管怎样进行分治,递归深度都是 \(O(n)\)

一种解决方法是对每个点的若干个儿子,我们新建 \(\text{deg}_i-1\) 个点把这颗树变成二叉树。如图:

图源 https://www.cnblogs.com/Miracevin/p/10430208.html

由于 \(\sum \text{deg}_i=O(n)\),所以我们可以加入 \(O(n)\) 个点让每个点的度数都不会太大。

但我们加入了一些点,这就会带来一些问题,比如说原树上两点的距离和新树上两点距离不相等之类的=_=

解决方法是把新加入的点边权值都设为 \(0\)。由此我们可以看到:边分治中加入的虚点虚边不能影响原树的信息统计。

当然,有付出就会有收获,边分治有很好的性质:一条边把树划分成了两个部分,这样一来,我们需要考虑的也只有两个集合,大大降低了实现难度。

边分治-例题

BZOJ2870 最长道路tree

这题有并查集+维护直径的简单做法,不过我们这里用边分治来做这道题。

考虑选一条边将树分为两个部分,\(f_u,g_u\) 分别表示 \(u\) 到分治中心对应侧节点的距离与路径上点权 \(\min\)

那么对于当前分治中心边 \((u,v)\),设 \(S(u)\)\(S(v)\) 分别为 \(u,v\) 各自侧节点所构成集合,答案就是

\[\max\{\min(g_x,g_y)\times (f_x+f_y+1)|x\in S(u),y\in S(v)\} \]

考虑把路径分为 \(g_x\ge g_y\)\(g_x\le g_y\) 两类,对于第一类我们枚举 \(g_y\),分别将左右两侧节点按 \(g\) 排序,然后维护一个指针计算 \(x\) 侧的最大值即可。

另一种同理,于是就做完了。复杂度 \(O(n\log ^2n)\)AC Code

动态点分治

对于一棵树 \(T\),考虑在点分治的过程中建出一棵新树 \(T_2\)

对于点分治过程中的一个当前根 \(r\) 与其儿子 \(v\) 子树的重心 \(t\),我们在新树 \(T_2\) 上连边 \(r\to t\)。这棵新树就是「点分树」。

点分树有一些性质:

  • 对于两个点 \(u,v\),其在点分树上的 LCA 必然在原树上两点的路径上。

这保证了我们点分治算法的正确性。

我们知道树上路径问题经常求出 LCA 然后把这条路径拆分成 \(u\to \text{LCA}\)\(\text{LCA}\to v\) 两段,再进一步处理。

而仔细思考一下可以发现实际上并不一定真的要从 LCA 这里分开。

点分治实际上就是从点分树上的 LCA 处把路径断开,然后进行统计。

  • 点分树的树高为 \(O(\log n)\)

这一点保证了点分治算法的复杂度。

如果直接在原树上枚举 LCA,那么复杂度为 \(O(\sum \text{size}(i))=O(nh)\),其中 \(h\) 为树高。

而点分树的树高为 \(O(\log n)\),那么复杂度就愉快地变成了 \(O(n\log n)\)

第二个性质尤为重要。

例如,我们现在要求一个点 \(x\) 到所有其他点的距离之和,即 \(\sum_v\text{dist}(x,v)\),那么考虑枚举 \(x\)\(v\)\(\text{LCA}\),我们在点分树上不断跳 \(x\) 的祖先 \(r\)

对于一对父子 \((r,fa_r)\),设 \(\text{Sum}(x)\) 表示 \(x\) 子树内所有点与 \(x\) 距离之和,那么贡献就是 \(\text{Sum}(fa_r)-\text{Sum}(r)\)

减掉 \(\text{Sum}(r)\) 的原因如果 \(x,v\)\(\text{LCA}\)\(fa_r\),那么 \(v\) 不可能在 \(r\) 的子树内。感觉理解普通点分治之后应该很容易理解这一点= =

由于 \(x\) 只有 \(O(\log n)\) 个祖先,该算法的复杂度是正确的。

点分树是原树的一棵重构树。注意在算贡献的过程中要分清原树和点分树=_=

动态点分治-例题

Luogu6329 震波 Present 5

仍然考虑枚举 \(x\) 的祖先 \(r\),然后计算父子对 \((r,fa_r)\) 的贡献。

\(f_1(u,j)\)\(u\) (在点分树上的)子树内与 \(u\) (在原树上的)距离 \(\le j\) 的所有点的点权之和。

为了去除 \(u\) (在点分树上的)子树的贡献, 还需要记录 \(f_2(u,j)\) 表示 \(u\) (在点分树上的)子树内与 \(fa_u\) (在原树上的)距离 \(\le j\) 的所有点的点权之和。

你可能会疑惑:\(f_2(u,j)\) 难道不就是 \(f_1(u,j-1)\) 吗?注意这里 \(u\)\(fa_u\) 在点分树上的距离(\(=1\))与在原树上的距离(未知)并不相等,因此需要分别记录。

那么查询 \(\text{ans}(x,k)\) 的时候,对于 \(x\) 的两个祖先 \((r,fa_r)\),其贡献为 \(f_1(fa_r,k-\text{dist}(x,fa_r))-f_2(r,k-\text{dist}(x,fa_r))\)

修改的时候也是同理。

考虑怎么维护 \(f_1,f_2\):我们发现修改时相当于一个单点加,查询相当于区间求和,那么可以使用树状数组维护。使用树剖来查询 \(\text{dist}\),总的时间复杂度为 \(O(n\log ^2n)\)

注意使用树状数组时需要动态开点,详细看代码。AC Code

Luogu2056 ZJOI2007 捉迷藏 Present 5

仍然考虑点分树,在修改的过程中动态维护答案。

我们相当于要维护树上的一个点集,支持加入/删除一个点,并查询该点集所构成虚树的直径。LCT

考虑建出点分树,当加入一个点 \(x\) 的时候,我们枚举这个点在点分树上的相邻祖先 \((r,fa_r)\) ,并计算以 \(fa_r\)\(\text{LCA}\) 的所有路径 \((x,v)\) 的贡献。

这里我们要求 \(v\) 不能在 \(r\) 的子树内,但需要在 \(fa_r\) 的子树内。

我们发现上题中由于求的是一个和式所以可以直接减掉 \(r\) 子树的贡献,但本题要求的是一个 \(\text{max}\) 形式,这东西不具有可减性,怎么办呢=_=

其实点分树是很灵活的一个东西!考虑在每个点 \(r\) 处维护一个堆 \(F(r)\),里面装上其子树内所有点与 \(fa_r\) 的距离,同时在 \(fa_r\) 处维护其所有子节点 \(v\)\(F(v)\) 堆顶所构成集合的最大与次大值之和即可。设该值为 \(S(r)\),那么答案就是所有 \(S(r)\) 中的最大值。

那么一个点改变颜色就相当于在堆里插入/删除一个数,使用 std::multiset 来实现堆即可......?

实测这样会 TLE 一个点,可以使用两个 priority_queue 来实现一个堆,减小常数。

具体来说,维护两个优先队列 \(Q_1,Q_2\),插入一个数 \(x\) 就直接把 \(Q_1.\text{push}(x)\),删除一个数 \(x\) 时我们把 \(x\) 扔进 \(Q_2\)

查询堆顶的时候,如果 \(Q_1.\text{top}()==Q_2.\text{top}()\),那么就一直弹掉两个堆的堆顶直到二者不同。

为什么这样是对的?其实这种方法叫做「懒惰删除」,一般的做法是另开一个 std::map 来维护已经删除的数,不过 map 常数仍然很大,因此可以使用另一个优先队列来实现。不难证明这样做的正确性。

AC Code

P3241 HNOI2015 开店 Present 5

考虑建出点分树,对于当前的询问 \((u,L,R)\),我们在点分树上不断跳其父节点 \((r,fa_r)\),然后统计 \(fa_r\) 的贡献减去 \(r\) 的贡献。

考虑在每个点 \(x\) 处维护两个动态开点线段树 \(T_1,T_2\),对于 \(x\)(在点分树上的)子树内的每个点 \(y\) 我们在 \(T_1\) 的下标 \(A_y\) 处加上 \(\text{dist}(x,y)\),在 \(T_2\) 下标 \(A_y\) 处加上 \(\text{dist}(fa_x,y)\),那么一对父子 \((r,fa_r)\) 的贡献就是 \(fa_r\) 的线段树 \(T_1\)\([L,R]\) 的区间和减去 \(r\) 的线段树 \(T_2\)\([L,R]\) 的区间和。

然后你发现这个东西的空间复杂度是 \(O(n\log ^2n)\) 而且常数巨大。。

其实这题没有修改,所以在每个点维护一个 vector 并且求个前缀后缀和之类的就完事了,这样空间复杂度是 \(O(n\log n)\),常数貌似还好?AC Code

CF337D Book of Evil Present 5

相当于给树上若干个点的点权 \(+1\),然后查询有多少个点距离 \(d\) 范围内的点权之和等于 \(m\)

建出点分树直接做就行了,时间复杂度 \(O(n\log ^2n)\)AC Code

posted @ 2022-07-16 15:49  云浅知处  阅读(472)  评论(0)    收藏  举报