树上启发式合并(dsu on tree)

树上启发式合并(dsu on tree)

简单介绍

在树上,对于一些离线无修改并且答案关乎子树的题目,我们可以使用树上启发式合并来优化

启发式合并是一种优雅的暴力,暴力在于他的统计答案是暴力的,而优雅就优雅在他有一个优秀且严格的复杂度

核心思路为:在重链剖分的基础上,对于每个节点,求解完所有轻儿子后再求解重儿子,并将重儿子的答案合并进当前节点中,所以当前节点只需再求解轻儿子即可。

简单伪代码即为:

dfs(x) {
    for(edge[x]) {
        int y = to[x];
        if(y != son[x]) {
            dfs(y);
            clear(y);
        }
    }
    dfs(son[x]);
    // no clear!!
    cal(x); // get answer in brute force
}

而对于复杂度,提供一个感性证明:

对于每个点 \(x\),被计算答案(进入dfs函数)的次数应为 (根到 \(x\) 的路径上轻边的数量+1),

而根据轻重边的性质,从根到任意点的路径上,轻边个数不超过 \(\log n\)

所以复杂度为 \(O(n\log n)\)

具体实现

以例题 CF600E Lomsat gelral 为例:

题意:一棵有根树,每个点有颜色,若以 \(x\) 为根的子树内颜色 \(c\) 出现的次数最多,则称为主导色(可有多个),计算每个节点为根的子树的主导色编号和。

在暴力解法中,我们可以遍历每一棵子树,在 \(O(n^2)\) 的复杂度内轻易解决这个问题,而使用树上启发式合并后,可以在 \(O(n\log n)\) 的时间内求解

具体实现与伪代码逻辑相同,不再做过多介绍:

// son[x] -> 重儿子
void dfs1(ll x, ll pre) { // init
	ll maxx = 0; siz[x] = 1;
	for(ll i = hd[x]; i; i = nxt[i]) {
		ll y = ver[i];
		if(y == pre) continue;
		dfs1(y, x);
		if(maxx < siz[y]) maxx = siz[y], son[x] = y;
		siz[x] += siz[y];
	}
}
void dfs3(ll x, ll pre, ll d) { // brute force
	cnt[c[x]]++;
	if(cnt[c[x]] > maxn) maxn = cnt[c[x]], sum = c[x];
	else if(cnt[c[x]] == maxn) sum += c[x];
	for(ll i = hd[x]; i; i = nxt[i]) {
		ll y = ver[i];
		if(y == pre || y == d) continue;
		dfs3(y, x, d);
	}
}
void dfs4(ll x, ll pre) { // clear
	cnt[c[x]]--;
	for(ll i = hd[x]; i; i = nxt[i]) {
		ll y = ver[i];
		if(y == pre) continue;
		dfs4(y, x);
	}
}
void dfs2(ll x, ll pre) { // main
	for(ll i = hd[x]; i; i = nxt[i]) {
		ll y = ver[i];
		if(y == pre || y == son[x]) continue;
		dfs2(y, x);
		dfs4(y, x);
		sum = maxn = 0;
	}
	if(son[x]) dfs2(son[x], x);
	dfs3(x, pre, son[x]);
	ans[x] = sum;
}

在使用该算法是一定要明确好操作顺序后再开始敲代码,尤其是各个dfs函数的顺序和对 \(sum\)\(maxn\) 等变量清空的时机等

应用

暂时按照该题单一道一道做,做完在下面补解法

CF208E 维护子树中各深度点的个数,离线处理答案

CF570D 与上题大致相同,改为记录各深度各字母的个数,最后离线处理答案时通过字母个数奇偶性判断回文

posted @ 2025-08-12 21:12  Hirasawayuiii  阅读(34)  评论(0)    收藏  举报