Skip to content

Commit 7fdc111

Browse files
authored
Create dsu-on-tree.md
1 parent a57d97f commit 7fdc111

1 file changed

Lines changed: 207 additions & 0 deletions

File tree

content/Blog/dsu-on-tree.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
Title: 树上启发式合并(DSU on Tree)
2+
Date: 2025-07-13
3+
Tags: 数据结构, 树形DP, 启发式合并, 树链剖分, DSU on Tree, Leetcode
4+
Slug: dsu-on-tree
5+
6+
## 什么是 DSU on Tree?
7+
8+
**DSU on Tree(Disjoint Set Union on Tree)**,中文称为“树上启发式合并”,是一种用于高效处理**树上子树统计类问题**的算法技巧。尽管其名称中含有 “DSU(并查集)”,但本质上与并查集并无直接关联。
9+
10+
该方法广泛应用于以下场景:
11+
12+
* 统计每个节点子树中的某类属性(如颜色种类、频次、权重等);
13+
* 在递归过程中合并不同子树的统计信息,显著提升效率;
14+
* 避免大量冗余的 insert/delete 操作所造成的性能浪费。
15+
16+
其核心策略结合了**启发式合并**(小的合并到大的)与**重儿子优先保留**的思想,与树链剖分中的轻重边划分具有一致性。
17+
18+
近年 LeetCode 也多次考察相关题目,说明该技巧即将成为面试中的高频考点(误
19+
20+
## 问题背景与动机
21+
22+
### 示例题目:树上统计颜色种类数
23+
24+
[题目链接][1]
25+
26+
> 给定一棵以 1 为根的树,每个节点有一个颜色。
27+
> 多次询问:以某个节点为根的子树中,有多少种不同的颜色?
28+
29+
输出:对于每个查询,输出对应子树中的颜色种类数。
30+
31+
> ⚠️ 注意:原题中颜色 c\[i] 可能为 0,代码中需进行特殊处理。
32+
33+
### 为什么不能直接使用树形 DP?
34+
35+
树形 DP 的典型做法是:递归处理每个子节点,将其统计结果合并到父节点。但这种策略在本问题中存在效率问题:
36+
37+
* 每次合并都需要对 map 或数组进行 insert/delete;
38+
* 若对子树数据进行暴力合并,可能会产生大量重复操作;
39+
* 最坏情况下,时间复杂度可能达到 O(n²)。
40+
41+
为解决这些问题,我们引入 **DSU on Tree**。通过对重儿子保留、轻儿子清除的启发式策略,可以将时间复杂度优化至 **O(n log n)**
42+
43+
## DSU on Tree 的算法流程
44+
45+
1. 预处理每个节点的子树大小,确定其重儿子(子树最大的孩子);
46+
2. 以 DFS 遍历整棵树,处理流程如下:
47+
48+
* 首先递归处理所有轻儿子,并在处理后清除其统计结构;
49+
* 然后递归处理重儿子,保留其统计结果;
50+
* 接着将所有轻儿子的统计信息合并到当前维护的结构中;
51+
* 将当前节点自身的信息加入统计;
52+
* 更新该节点的答案。
53+
54+
该策略确保:**每个节点的信息最多被合并 log n 次**,大幅降低了总体复杂度。
55+
56+
## 算法原理与复杂度分析
57+
58+
### 正确性说明
59+
60+
DSU on Tree 在整体结构上与树形 DP 一致,都是自底向上合并子树信息。不同之处在于合并顺序与数据结构管理策略更具启发性,从而降低了时间复杂度。其正确性自然继承于递归式的子树遍历结构。
61+
62+
### 重/轻儿子与边的定义
63+
64+
对于每个节点 $u$:
65+
66+
* **重儿子**:其所有子节点中,子树大小最大的一个;
67+
* **轻儿子**:其余所有子节点;
68+
* **重边**:$u$ 与其重儿子之间的边;
69+
* **轻边**:$u$ 与轻儿子之间的边。
70+
71+
这一划分方式与树链剖分中的定义完全一致,旨在最大程度复用统计结构,减少数据迁移成本。
72+
73+
### 为什么每个节点最多被合并 $\log n$ 次?
74+
75+
设某节点从根开始向下,在 DFS 过程中每经过一条轻边,子树规模至多减半:
76+
77+
$$
78+
\frac{n}{2^x} \geq 1 \Rightarrow x \leq \log_2 n
79+
$$
80+
81+
因此,一个节点作为被合并对象,最多经历 $\log n$ 次合并操作,从而将总体复杂度限制在 $O(n \log n)$ 范围内。
82+
83+
### 重边的作用
84+
85+
重边连接的是子树最大的孩子。在合并过程中,我们选择保留重儿子的统计结构,不清空、不重建,从而实现结构的复用和效率提升。
86+
87+
## 伪代码示例
88+
89+
```python
90+
# 全局变量说明:
91+
# g[u]:邻接表表示的树结构
92+
# sz[u]:以 u 为根的子树大小
93+
# big[u]:u 的重儿子(子树最大的孩子)
94+
# col[u]:每个节点的颜色
95+
# L[u], R[u]:u 的 DFS 序区间
96+
# Node[i]:DFS 序编号 i 对应的节点编号
97+
# cnt[c]:颜色为 c 的节点出现次数
98+
# totColor:当前颜色种类数
99+
# ans[u]:以 u 为根的子树的答案
100+
101+
def dfs0(u, parent):
102+
global time
103+
time += 1
104+
L[u] = time
105+
Node[time] = u
106+
sz[u] = 1
107+
for v in g[u]:
108+
if v == parent:
109+
continue
110+
dfs0(v, u)
111+
sz[u] += sz[v]
112+
if big[u] is None or sz[v] > sz[big[u]]:
113+
big[u] = v
114+
R[u] = time
115+
116+
def add(u):
117+
color = col[u]
118+
if cnt[color] == 0:
119+
totColor += 1
120+
cnt[color] += 1
121+
122+
def remove(u):
123+
color = col[u]
124+
cnt[color] -= 1
125+
if cnt[color] == 0:
126+
totColor -= 1
127+
128+
def get_answer():
129+
return totColor
130+
131+
def dfs1(u, parent, keep):
132+
# 处理所有轻儿子并清除其贡献
133+
for v in g[u]:
134+
if v == parent or v == big[u]:
135+
continue
136+
dfs1(v, u, keep=False)
137+
138+
# 处理重儿子,保留其统计数据
139+
if big[u] is not None:
140+
dfs1(big[u], u, keep=True)
141+
142+
# 把所有轻儿子的 DFS 区间数据合并进来
143+
for v in g[u]:
144+
if v == parent or v == big[u]:
145+
continue
146+
for i in range(L[v], R[v] + 1):
147+
add(Node[i])
148+
149+
# 加入当前节点自身
150+
add(u)
151+
152+
# 保存当前节点答案
153+
ans[u] = get_answer()
154+
155+
# 若不保留此子树信息,则清除
156+
if not keep:
157+
for i in range(L[u], R[u] + 1):
158+
remove(Node[i])
159+
160+
```
161+
162+
### 时间复杂度分析
163+
164+
* 每个节点的信息最多被合并 $\log n$ 次;
165+
* 每次合并操作为 $O(1)$(若使用数组)或 $O(\log n)$(若使用 map);
166+
* 因此总复杂度为:$ O(n \log n) $
167+
168+
对于多数题目中,颜色值范围有限时可使用数组,使整体操作更接近线性。
169+
170+
## 例题:LeetCode3575 - Maximum Good Subtree Score
171+
172+
🔗 [题目链接](https://leetcode.com/problems/maximum-good-subtree-score/description/)
173+
174+
### 题意简述
175+
176+
给定一棵以 0 为根的树,每个节点有一个整数权值 `vals[i]`,及其父节点 `par[i]`。一个子树中,若**任意子集**中所有节点的十进制表示中每个数字最多出现一次,该子集为“合法子集”。合法子集的得分为其节点权值之和。子集可以不连通。
177+
178+
定义 `maxScore[u]` 表示以节点 `u` 为根的子树中,合法子集的最大得分。求所有节点的 `maxScore[u]` 之和。
179+
180+
### 解法思路
181+
182+
这是典型的 DSU on Tree 应用场景,适用于子树统计类问题:
183+
184+
* 每个节点 DFS 时记录子树中数字出现情况(用 bitmask);
185+
* 合并时保留重儿子的统计结构,将轻儿子信息逐步合并进来;
186+
* 遇到重复数字时及时剪枝;
187+
* 使用“重儿子保留,轻儿子清除”策略,确保总复杂度不超限。
188+
189+
总复杂度控制在 $O(n \log n)$,显著优于暴力枚举或者树形DP。
190+
191+
## 小结
192+
193+
**DSU on Tree 是解决树上子树信息统计类问题的高效工具**,其主要优势包括:
194+
195+
* 避免重复统计,提升合并效率;
196+
* 时间复杂度优秀,达 $O(n \log n)$;
197+
* 与树链剖分的轻重边思想互补;
198+
* 实用性强,广泛应用于竞赛与工程题中。
199+
200+
## 参考资料
201+
202+
* [OI Wiki:树上启发式合并](https://oi-wiki.org/graph/dsu-on-tree/)
203+
* [LeetCode3575 - Maximum Good Subtree Score](https://leetcode.com/problems/maximum-good-subtree-score/description/)
204+
* [Luogu U41492 树上数颜色][1]
205+
206+
[1]: https://www.luogu.com.cn/problem/U41492
207+

0 commit comments

Comments
 (0)