树分治

树分治有多种。

点分治

用于处理树上路径问题。

选择一个点作为当前的 root ,把路径分为两种:

  1. 经过 root 的路径
  2. 不经过 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) \]

:这是显然的,自证不难。

代码

懒得写了

边分树

学这个干什么

posted @ 2022-04-16 16:29  Gyan083  阅读(53)  评论(0编辑  收藏  举报