树分治
树分治有多种。
点分治
用于处理树上路径问题。
选择一个点作为当前的 root ,把路径分为两种:
- 经过 root 的路径
- 不经过 root 的路径
对于第 1 类路径,我们可以暴力 dfs 统计对答案的贡献。
对于第 2 类路径,它一定是在 root 的某一棵子树内,于是我们把 root 删掉,让原来的一整棵树变成一片森林,然后对森林里的每棵树依次再做点分治,递归处理。
可以证明,每次点分治时, root 取当前树的重心可以获得最优复杂度。
递归时,每次找重心 \(O(n)\) ,点分治最多递归 \(\log n\) 层,所以全部时间复杂度 \(O(n\log n)\)
若每次计算答案是 \(O(T)\) 的,则时间复杂度为 \(O(Tn\log n)\)
代码模板
#include <algorithm>
#define N 114514 //树中的点数
bool del[N];//删除标记
//子树大小,最大的子树大小
int siz[N], mss[N]={0x7f7f7f7f};//size,max size of sons
int root, total;
//计算重心
void getroot(int x, int fr) {
siz[x] = mss[x] = 1;
for (int e=head[x], y=to[e]; e; y=to[e=nxt[e]]) {
if (y == fr || del[y]) continue;
getroot(y, x);
siz[x] += siz[y];
if (mss[x] < siz[y])
mss[x] = siz[y];
}
mss[x] = std::max(mss[x], total - siz[x]);//x的子树包括 向上的子树、向下的子树
if (mss[x] < mss[root]) root = x;
}
//计算重心
inline int getroot(int x) {
total = siz[x] ? siz[x] : n, root = 0;
getroot(x, 0);
return root;
}
//计算以 x 为根的子树对答案的贡献
inline void calc(int x) {
//TODO:...
}
void dfz(int x) {
x = getroot(x);//IMPORTANT!!! 找重心
calc(x);//计算更新答案
del[x] = 1;//标记删除节点
for (int e=head[x], y=to[e]; e; y=to[e=nxt[e]]) {
if (del[y]) continue;
dfz(y);//分治
}
}
signed main() {
//...
dfz(1);
//...
return 0;
}
边分治
与点分治类似。
但感觉没啥用。
听说会被菊花图卡,于是我们需要在原图中加一些新边。好像比点分治复杂些...
不过有种神妙的方法似乎可行:把每条边裂成两条边,中间插一个点,原来的边权变成这个点的点权,然后就把边分治转化成了点分治???
点分树
重构树的形态,使得原本一些糟糕的算法可以拥有正确的复杂度。
使用前提:不考虑原树形态。
分析
模仿点分治每次找重心的方法重构树。
每次找到的重心与上一层的重心重新连边成为父子。
这样就可以让树稳定于 \(\log n\) 层。
怎么我突然想到了平衡树?
点分树还支持修改操作,与点分治不同,点分树是在线数据结构。
补充:
定理:对于任意两点 \(u,v\) ,唯一可以确定的是, \(u,v\) 在 点分树 上的 LCA 一定在 原树 中 \(u\) 到 \(v\) 的路径上。即
\[\text{设 }T=\operatorname{lca_{点分树上}}(u,v)\\
\operatorname{dis_{原树上}}(u,v)=\operatorname{dis_{原树上}}(u,T)+\operatorname{dis_{原树上}}(T,v)
\]
证:这是显然的,自证不难。
代码
懒得写了
边分树
学这个干什么
本文来自博客园,作者:Gyan083,转载请注明原文链接:https://www.cnblogs.com/gyan083/p/16153291.html