20251208树上启发式合并总结

树上启发式合并

启发式合并算法通过将高度较小的树作为高度较大树的子树进行优化。

树上启发式合并在处理离线问题时,其效率往往优于或等同于其他算法,同时具备更容易理解和实现的优势。

传统解决方案通常依赖复杂的数据结构,但对于可离线处理的问题,是否存在更简便的方法?

离线处理的核心在于通过预处理实现O(1)O(1)O(1)时间复杂度的查询输出。

直接暴力预处理的时间复杂度为O(n2)O(n²)O(n2),即对每个子节点执行一次遍历(每次遍历复杂度与n同阶),n个节点导致总复杂度为O(n2)O(n²)O(n2)。

注意到每个节点的答案可由其子树及自身信息推导得出,这一特性可用于优化处理。

预处理阶段可以O(n)O(n)O(n)时间复杂度完成:

  1. 计算每个节点子树大小
  2. 确定重儿子(与树链剖分类似,即拥有最多子节点的儿子)

定义符号说明:

  • cnticnt_icnti:颜色 i 的出现次数
  • ansuans_uansu:节点u的答案

节点遍历流程:

  1. 优先遍历轻儿子(非重儿子),计算答案但不保留其对cnt数组的影响
  2. 遍历重儿子,保留其对cnt数组的影响
  3. 再次遍历轻儿子子树节点,累计其贡献以确定节点u的最终答案

U41492 树上数颜色

模板。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
vector<int> E[100005];
int n,c[100005],sz[100005],son[100005],l[100005],r[100005],dfn[100005],ans[100005],cnt[100005],tot,col;
void dfs(int x,int fa){
	dfn[++tot]=x;
	l[x]=tot;
	sz[x]=1;
	for(int i=0;i<E[x].size();i++){
		int v=E[x][i];
		if(v==fa)continue;
		dfs(v,x);
		sz[x]+=sz[v];
		if(sz[v]>sz[son[x]]){
			son[x]=v;
		}
	}
	r[x]=tot;
}
void dfs1(int x,int fa,bool f){
	for(int i=0;i<E[x].size();i++){
		int v=E[x][i];
		if(v==fa||son[x]==v)continue;
		dfs1(v,x,0);
	}
	if(son[x]){
		dfs1(son[x],x,1);
	}
	for(int i=0;i<E[x].size();i++){
		int v=E[x][i];
		if(v==fa||son[x]==v)continue;
		for(int j=l[v];j<=r[v];j++){
			int vv=dfn[j];
			if(!cnt[c[vv]]){
				col++;
			}
			cnt[c[vv]]++;
		}
	}
	if(!cnt[c[x]]){
		col++;
	}
	cnt[c[x]]++;
	ans[x]=col;
	if(!f){
		for(int i=l[x];i<=r[x];i++){
			int v=dfn[i];
			cnt[c[v]]--;
			if(!cnt[c[v]]){
				col--;
			}
		}
	}
}
int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		E[u].push_back(v);
		E[v].push_back(u);
	}
	for(int i=1;i<=n;i++){
		cin>>c[i];
	}
	dfs(1,0);
	dfs1(1,0,0);
	int q;
	cin>>q;
	while(q--){
		int x;
		cin>>x;
		cout<<ans[x]<<endl;
	}
	return 0;
}