1. DSU on Tree 在子树众数计数 (CF 600E Lomsat gelral) 的 O(n log n) 重儿子保留策略。
DSU on Tree(树上启发式合并)如何求解 CF 600E Lomsat Gelral——统计每个子树中出现次数最多的颜色编号之和?"重儿子保留"策略为什么能把朴素 O(n²) 的暴力降到 O(n log n)?
- 轻儿子暴力统计、重儿子结果保留的遍历顺序
- 每个节点作为轻子树被重复遍历 O(log n) 次的总复杂度论证
- 全局桶状态复用与"清空"时机的工程实现
DSU on Tree 的核心是"保留重儿子、暴力轻儿子"。对每个节点 u,先递归处理所有轻儿子子树且每棵处理完即清空桶,再递归处理重儿子子树并保留其桶状态,最后把 u 的轻儿子子树中所有节点逐个加入桶,此时桶恰好包含以 u 为根的整棵子树信息,据此回答 u 的查询;若 u 是父亲的轻儿子,则整棵子树结束后整体清空桶。一个节点被"暴力加入"的次数等于它到根路径上的轻边数量,由重链剖分性质可知任意节点路径上的轻边数不超过 O(log n),因此每个节点至多被重复统计 O(log n) 次,总复杂度 O(n log n)。相比朴素 O(n²) 的逐子树统计,关键收益在于重儿子的桶被直接继承、完全避免重复统计。
本题考察"摊还思想在树上的应用":把每个子树独立统计转化为"复用全局桶 + 只清空轻子树",与线段树合并、树上差分同属处理子树信息的经典手段。回答时先讲清递归顺序(轻儿子清空、重儿子保留、轻儿子回填、必要时清空),再用轻边数量论证复杂度,最后说明常数与实现细节(用 dfn 序数组批量加入子树节点可避免递归收集)。
void dfs(int u, int fa, bool keep) {
for (v : g[u]) if (v != fa && v != son[u]) dfs(v, u, false);
if (son[u]) dfs(son[u], u, true);
for (v : g[u]) if (v != fa && v != son[u])
for (int i = dfn[v]; i < dfn[v] + sz[v]; ++i) add(col[ord[i]]);
add(col[u]);
ans[u] = cur_sum; // 桶中恰为整棵子树信息
if (!keep) for (i : dfn[u]...dfn[u]+sz[u]-1) del(col[ord[i]]); // 清空
}