点分治 & 边分治

更好的阅读体验

序列上的简单分治操作很好写,每次找一个中间点 mid=(l+r)/2,然后分别处理左右区间再合并答案,但是在树上我们不能简单的取到一棵树的中点,所以就有了这两种方法来在树上进行分治。

点分治

每次的中点选择子树的重心,即让分出的两棵子树 \(size\) 的最大值最小,这样分治就会尽量的平衡。

P3806 【模板】点分治1

给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。

考虑每次如何处理一棵子树的答案。

枚举以这棵树的根为路径的折点,显然可以在遍历一棵子树之后检查是否有某两棵子树里的两段加起来等于 \(k\) ,这样如果暴力按照 dfs 序选择根的话,这样的时间复杂度显然是 \(\mathcal O(n^2)\) 的。

但是如果每次选择所有子树里的重心进行递归处理,时间复杂度就能降到 \(\mathcal O(n\log n)\)

稍微证明一下。

我们把重心分层,第 \(i\) 层重心递归子树找到的重心记为 \(i+1\) 层重心。

显然每一层重心的所有子树刚好覆盖了整棵树,但是每次往下一层之后,大小为 \(n\) 的树就会分为大小至多为 \(\frac{n}{2}\) 的树,这样的分裂至多进行 \(\log n\) 次,所以总时间复杂度是 \(\mathcal O(n\log n)\)

于是就可以 找到子树的重心->计算这个点为根的答案->向上合并答案->递归重心为根 求解了。

看起来及其暴力,时间复杂度却可以保证。

完整代码Link

边分治

每次选择一条,让两边的子树 \(size\) 的最大值最小,然后递归处理两边的子树。

有个显然的结论是菊花图可以卡这种做法到 \(\mathcal O(n^2)\)

于是我们就像下图一样重新建一下树:

这样就可以保证边分治的时间复杂度是稳定的 \(n\log n\) 而不是偶尔的 \(n^2\) 了。

P4565 [CTSC2018]暴力写挂

给定两棵 \(n\) 个结点的树 \(T,T'\),求 \(max\{dep_x+dep_y-dep_{lca_{x,y}}-dep'_{lca'_{x,y}}\}\)

\(dep'\) 表示第二棵树里的深度,\(lca'\) 表示第二棵树上的 LCA 。

改写一下式子,\(\frac{dep_x+dep_y+dis(x,y)}{2}-dep'_{lca'}\)

假设边分治的时候边 \(i\) 两端作为子树根的点是 \(a,b\) ,边权是 \(val_i\),边分到的点 \(x,y\) 分别在 \(a,b\) 两棵子树里,前面那部分的分子就是 \(dep_x+dis(x,a)+val_i+dep_y+dis(y,b)\)

发现有重复的部分,我们令 \(w(x)=dep_x+dis(x,rt)\)\(rt\) 是子树的根。

那么就有 \(w(x)+w(y)+val_i\) 就是前面的分子。

\(a,b\) 子树里的点分别打上黑/白标记,然后把这两棵子树的点在第二棵树里拉出来跑虚树,这样就可以在虚树上 dp 。

记录 f[u][0]表示 u 的子树里白点的最大 \(w(x)\)f[u][1]则表示黑点的最大值。

转移的时候 f[u][0]=max(f[u][0],f[v][0]),f[u][1]=max(f[u][1],f[v][1]);,但是在转移之前我们就可以统计答案。

由于是在虚树上 dp,所以虚树上 u 的两棵不同子树里的点的 LCA 都是 u,那么在转移之前我们就有 ans=max(ans,(f[u][0]+f[v][1]+len)/2-dep2[u]),ans=max(ans,(f[u][1]+f[v][0]+len)/2-dep2[u]);,其中 len 是边分治的边权。

那么思路就很明确了:重构第一棵树,在上面边分治找到一条边 \((a,b,w)\) ,把 \(a,b\) 的子树拉出来在第二棵树上建虚树,在上面 dp 统计答案,然后继续分治。

完整代码Link

posted @ 2021-05-19 14:26  摸鱼酱  阅读(110)  评论(0编辑  收藏  举报