树上启发式合并学习笔记

引入

考虑这样一类允许离线树上问题:
对于一棵静态有根树上的所有点,维护有关节点子树的某些信息(如:统计子树内颜色种类数、出现次数最多的颜色、路径异或和、满足某种条件的点对数量等),且这些信息可以通过合并子树信息得到

显然暴力可以每次遍历每个节点的子树中的所有节点统计答案,但是这样做的复杂度显然就至少是 O(n2)O(n^2)O(n2) (“至少”是因为可能合并答案不是 O(1)O(1)O(1) 的)。于是考虑不在遍历,每个节点的时候都清空数据,而是在统计节点 uuu 的答案时直接从某个儿子 vvv 的答案合并而来。

树上启发式合并

算法流程

沿用树剖定义:

  • 重子节点 &\&& 轻子节点:一个节点的子节点中子树最大的那个称为重子节点,如果有多个子树大小一样的取其一(如果是叶子节点的话重子节点默认为 000)。轻子节点是非重子节点的所有其他节点
  • 重边 &\&& 轻边:重边是从重子节点到父节点的边,轻边是除重边以外的所有边
  • 重链:由重边首尾相接的链是重链,有时候为了便于理解把是轻子节点的叶子也视为重链。

首先一遍 dfs 求出重儿子,代码如下:

void dfs(int u,int fa){
	siz[u] = 1;
	for(auto v: g[u]){
		if(v == fa) continue;
		dfs(v,u),siz[u] += siz[v];
		if(siz[v] > siz[son[u]]) son[u] = v;//记录重儿子
	}
}

然后进入树上启发式合并(dsu on tree)

先遍历 uuu 的轻儿子,计算答案,但不保留它对当前答案(状态)的影响。
然后遍历它的重儿子,保留它对当前答案的影响。
最后再次遍历 uuu 的轻儿子的子树,加入这些子树的贡献,得到 uuu 的答案。

核心代码如下:

void work(int u,int fa){
	ins(u);//加入节点 u
	for(auto v : g[u]){
		if(v == fa) continue;
		work(v,u);
    }
}
void dsu(int u,int fa){
	if(!son[u]){
		ins(u),/*do something*/;//u 是叶子结点,直接加入并统计答案
		return;
	}
	for(auto v : g[u]){
		if(v == fa || v == son[u]) continue;
		dsu(v,u);
		init();//清空当前答案
	}
	dsu(son[u],u);//保留重儿子的答案
	for(auto v : g[u]){
		if(v == fa || v == son[u]) continue;
		work(v,u);//加入其他子树答案
	}
	ins(u),/*do something*/;//加入 u 并统计答案
}

时间复杂度

注意到,对任意一个结点 uuu,它在 dsu 中被暴力插入/删除的充要条件是:uuu 所在的这条到根的路径上,至少有一条轻边被展开。换句话说,只有在 uuu 的某个祖先 ppp 满足 ppp 的轻儿子分支包含了 uuuuuu 才会在 dsu 到 ppp 时被扫一遍。

而轻边次数是有上界的。一个重链剖分经典结论是,对于任意结点 uuu,从 uuu 向上到根的路径上,轻边条数 ≤log⁡2n\leq \log_2 nlog2n。因为每走一条轻边,子树大小至少减半。

设一个结点的一次插入/删除是 O(k)O(k)O(k) 的,节点 uuu 在整个算法运行期间最多被插入/删除 2×(log⁡2n+1)2 \times (\log_2n + 1)2×(log2n+1) 次(+1+1+1 是因为 uuu 自身也可能被当作轻儿子展开,乘 222 是因为先插入后删除算两次)。于是每个点的总代价是 O(klog⁡2n)O(k \log_2 n)O(klog2n)。全树有 nnn 个点,总代价即为 O(nklog⁡2n)O(nk \log_2 n)O(nklog2n)

kkk 通常是 O(1)O(1)O(1) 的,于是就像我们说树剖时间复杂度为 O(nlog⁡2n)O(n \log_2n)O(nlog2n) 一样,启发式合并复杂度也为 O(nlog⁡2n)O(n \log_2n)O(nlog2n)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值