Loading

树上启发式合并(dsu on tree)学习笔记

严格上来说,dsu on tree 是一种遍历方式。

有时,我们不得不完整遍历每棵子树来找出每个子树的信息,或者说没有比遍历子树更优的方案,就可以尝试使用 dsu on tree 来遍历,可以直接 \(n ^ 2 \to n \log n\)

例题 CF600E,让你求每个子树的所有众数之和。

初始做法:暴力遍历每棵子树。\(n ^ 2\)

进阶做法:先处理 dfs 序,转化为序列上的区间查询,莫队求解。

树上启发式合并

考虑普通的 DP 流程,即对每个点开一个桶,暴力转移。这样时空都是 \(n ^ 2\) 双双爆炸,这还不如直接暴力遍历每棵子树。

那么我们考虑开一个全局的桶 \(buc\),记录每个颜色出现了多少次,维护当前桶中的众数之和是多少。

那么考虑以下流程,调用 \(solve(root)\),定义 \(solve(u)\) 函数如下(每个节点只取一个重儿子):

  • 对于 \(u\) 的所有非重儿子 \(v\):清空 \(buc\)\(solve(v)\)

  • 清空 \(buc\)

  • 如果 \(u\) 不是叶子,对于 \(u\) 的重儿子 \(v_0\)\(solve(v_0)\)

  • 然后通过遍历 \(v_0\) 之外所有 \(u\) 的子树,将这些点加入 \(buc\) 中。

  • \(u\) 的信息加入 \(buc\),那么此时 \(buc\) 中的和即为 \(u\) 的答案。

不难发现,这样相对于朴素的遍历,每个节点的重儿子少遍历的一次,我们来证明它的时间复杂度是 \(\mathcal{O(n \log n)}\) 的。

时间复杂度证明

考虑贡献法,节点 \(u\) 被遍历的次数应该是 \(u 到 root 的轻边数量 + 1\)

然后一个点到 \(root\) 的轻边数量不超过 \(\log n\),所以总复杂度是 \(\mathcal{O(n \log n)}\) 的。

CF600E

// Code by GENX.
// 2026-06-04
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 7;
int n, a[N], sz[N], ans[N], son[N];
vector<int> g[N];

struct buc{
	int cnt[N], res, mx;
	queue<int> q;
	void init(){
		while(q.size()) cnt[q.front()] --, q.pop();
		mx = res = 0;
	}
	void add(int x){
		cnt[x] ++, q.push(x);
		if(cnt[x] > mx) mx = cnt[x], res = x;
		else if(cnt[x] == mx) res += x;
	}
} Misaka;

void cal(int u, int pre){
	sz[u] = 1;
	for(int v: g[u]){
		if(v != pre){
			cal(v, u);
			sz[u] += sz[v];
			if(sz[v] > sz[son[u]]) son[u] = v;
		}
	}
}

void ext(int u, int pre){
	Misaka.add(a[u]);
	for(int v: g[u]){
		if(v != pre) ext(v, u);
	}
}

void dfs(int u, int pre){
	for(int v: g[u]){
		if(v != pre && v != son[u]){
			Misaka.init(), dfs(v, u);
		}
	}
	Misaka.init();
	if(son[u]) dfs(son[u], u);
	for(int v: g[u]){
		if(v != pre && v != son[u]) ext(v, u);
	}
	Misaka.add(a[u]);
	ans[u] = Misaka.res;
}

signed main(){
	ios::sync_with_stdio(0), cin.tie(0);
	
	cin >> n;
	for(int i = 1; i <= n; i ++) cin >> a[i];
	for(int i = 1; i < n; i ++){
		int u, v; cin >> u >> v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	
	cal(1, 0);
	dfs(1, 0);
	
	for(int i = 1; i <= n; i ++) cout << ans[i] << " ";
	
	return 0;
}
posted @ 2026-06-05 18:13  Trent900  阅读(48)  评论(0)    收藏  举报