LeetCode每日一题2025-05-23
3068. 最大节点价值之和 H
给你一棵 n 个节点的 无向 树,节点从 0 到 n - 1 编号。树以长度为 n - 1 下标从 0 开始的二维整数数组 edges 的形式给你,其中 edges[i] = [ui, vi] 表示树中节点 ui 和 vi 之间有一条边。同时给你一个 正 整数 k 和一个长度为 n 下标从 0 开始的 非负 整数数组 nums ,其中 nums[i] 表示节点 i 的 价值 。
Alice 想 最大化 树中所有节点价值之和。为了实现这一目标,Alice 可以执行以下操作 任意 次(包括 0 次):
- 选择连接节点
u和v的边[u, v],并将它们的值更新为:nums[u] = nums[u] XOR knums[v] = nums[v] XOR k
请你返回 Alice 通过执行以上操作 任意次 后,可以得到所有节点 价值之和 的 最大值 。
示例 1:

输入:nums = [1,2,1], k = 3, edges = [[0,1],[0,2]]
输出:6
解释:Alice 可以通过一次操作得到最大价值和 6 :
- 选择边 [0,2] 。nums[0] 和 nums[2] 都变为:1 XOR 3 = 2 ,数组 nums 变为:[1,2,1] -> [2,2,2] 。
所有节点价值之和为 2 + 2 + 2 = 6 。
6 是可以得到最大的价值之和。
示例 2:

输入:nums = [2,3], k = 7, edges = [[0,1]]
输出:9
解释:Alice 可以通过一次操作得到最大和 9 :
- 选择边 [0,1] 。nums[0] 变为:2 XOR 7 = 5 ,nums[1] 变为:3 XOR 7 = 4 ,数组 nums 变为:[2,3] -> [5,4] 。
所有节点价值之和为 5 + 4 = 9 。
9 是可以得到最大的价值之和。
示例 3:

输入:nums = [7,7,7,7,7,7], k = 3, edges = [[0,1],[0,2],[0,3],[0,4],[0,5]]
输出:42
解释:Alice 不需要执行任何操作,就可以得到最大价值之和 42 。
提示:
2 <= n == nums.length <= 2 * 10⁴1 <= k <= 10⁹0 <= nums[i] <= 10⁹edges.length == n - 1edges[i].length == 20 <= edges[i][0], edges[i][1] <= n - 1- 输入保证
edges构成一棵合法的树。
问题分析
给定一棵大小为 的无向树,节点编号为 ,以及一个正整数 和节点价值数组 。允许的操作是:任选一条边 ,对其两端的节点同时做一次 操作:
可以重复上述操作任意次。目标是最大化所有节点价值之和:
由于树上每次操作同时影响两个相邻节点,整个过程等价于对每个节点决定“最终是否被翻转奇数次”。记节点 的最终状态位为 ,若 则节点 的价值由 ,否则保持不变。每条被选的边 会让 和 同时翻转一次,因此如果对边 选择了奇数次,就相当于让 与 最终状态异或一次。
- 将这些约束在整棵树上传播,实际上对于树结构,可以定义一个根节点(取 号),并令每个节点状态由其子树的选择来决定。
- 记对于子节点 ,若整个以 为根的子树中翻转为奇数的子边数为奇数,则子树在“奇偶”方面会有影响。需要合并所有子节点对当前节点的贡献。
故而可使用“树形 DP”解决:定义状态
在对节点 做 DFS 递归时,遍历所有子节点 ,先计算其 ,然后令该子树的基础贡献为
并收集每个子节点“若翻转 的状态,相对于不翻转的增益”
把所有 分成正数类和负数类,用 表示所有 的和, 表示 的数量, 表示正类中最小值, 表示负类中最大值。若选择令 的子边翻转次数总体保持“偶数”,则贡献为
否则(即翻转奇数次),需要从正类中去掉最小的 或者从负类中加入最大的 ,取更优,即
然后根据 来决定 的子边翻转后与当前 的奇偶如何匹配,依此更新 与 。最终根节点 必须保证其 “父边不选” (即 ),答案即为 。
算法思路
-
建图与初始化
- 用邻接表 存储树结构。
- 创建二维数组 ,初始均为 。
-
DFS 递归
- 对每个节点 ,记录其父节点 ,避免回溯。
- 对所有子节点 递归调用
dfs(v, u),获得 。 - 计算子树基础贡献
- 列表
deltas存储 。 - 按照 正负分组,计算:
1
2
3
4
5sumP = ∑_{Δ_v ≥ 0} Δ_v
cntP = |{v | Δ_v ≥ 0}|
minP = min{Δ_v | Δ_v ≥ 0} (若不存在正类则为 +∞)
maxN = max{Δ_v | Δ_v < 0} (若不存在负类则为 −∞)
parityP = cntP mod 2 - 计算两种子边翻转奇偶情况下的贡献:
1
2
3
4
5contrib_same = base_sum + sumP
contrib_flip = max(
base_sum + (sumP - minP) if cntP > 0,
base_sum + (sumP + maxN) if maxN > −∞
) - 若
parityP == 0,则子边“偶数”对应contrib_same,子边“奇数”对应contrib_flip;否则相反。 - 最后考虑 自身是否翻转:
- 对于 (),若选择子翻转为“偶数”,则价值为 ;
如果选择为“奇数”,则价值为 。取两者最大。 - 对于 (),若子翻转为“偶数”,则价值为 ;
如果为“奇数”,则为 。同样取最大。
- 对于 (),若选择子翻转为“偶数”,则价值为 ;
-
返回结果
- 在根节点 上调用
dfs(0,−1),最终 即为整棵树在允许任意操作后能够获得的最大节点价值之和。
- 在根节点 上调用
时间复杂度
- 构建邻接表花费 。
- DFS 过程对每个节点只访问一次,且对每个节点需要遍历其所有邻居并做常数时间的计算。合并子节点差值时,只做 次更新。
- 整体每条边被访问两次,总共 。
- 因此总时间复杂度为 ,空间复杂度为 。
代码分解
- 图的表示与初始化
1
2
3
4
5
6n = len(nums)
g = [[] for _ in range(n)]
for u, v in edges:
g[u].append(v)
g[v].append(u)
dp = [[0, 0] for _ in range(n)] - DFS 主逻辑函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40def dfs(u, parent):
base_sum = 0
deltas = []
for v in g[u]:
if v == parent: continue
dfs(v, u)
base_sum += dp[v][0]
deltas.append(dp[v][1] - dp[v][0])
if not deltas:
dp[u][0] = nums[u]
dp[u][1] = nums[u] ^ k
return
sumP, cntP = 0, 0
minP, maxN = float('inf'), -float('inf')
for d in deltas:
if d >= 0:
sumP += d
cntP += 1
minP = min(minP, d)
else:
maxN = max(maxN, d)
parityP = cntP % 2
contrib_same = base_sum + sumP
contrib_flip = -float('inf')
if cntP > 0:
contrib_flip = max(contrib_flip, base_sum + (sumP - minP))
if maxN > -float('inf'):
contrib_flip = max(contrib_flip, base_sum + (sumP + maxN))
if parityP == 0:
child_parity0 = contrib_same
child_parity1 = contrib_flip
else:
child_parity1 = contrib_same
child_parity0 = contrib_flip
cand0_even = child_parity0 + nums[u]
cand0_odd = child_parity1 + (nums[u] ^ k) if child_parity1 > -float('inf') else -float('inf')
dp[u][0] = max(cand0_even, cand0_odd)
cand1_even = child_parity0 + (nums[u] ^ k)
cand1_odd = child_parity1 + nums[u] if child_parity1 > -float('inf') else -float('inf')
dp[u][1] = max(cand1_even, cand1_odd) - 调用与返回
1
2dfs(0, -1)
return dp[0][0]
代码实现
1 | from collections import defaultdict |
问题分析2
在任意次操作条件下,每条边选一次将使其端点同时做一次 。若我们将“最终节点 被 的次数模 2”记为状态 ,则每次对边 的选择相当于对 做一次异或操作。由于树的边集形成的线性系统有唯一约束:
即最终被翻转奇数次的节点数必须为偶数(因为每条被选的边为两个端点各贡献一次翻转,总的翻转次数为偶数)。这样,问题就简化为:
在满足翻转节点数为偶数的约束下,决定哪些节点 最终翻转,使得总价值
最大。
令每个节点 的差值为
若我们忽略约束,则只需对所有 的节点都设 ;但由于必须保证 为偶数,如果正差选取的数量为奇数,则需要“舍弃一个正差中最小的”或“额外加入一个负差中最大的”来翻转奇偶,取其中损失最小的一种。
因此,该方法不需要显式地做树形 DP,而是把所有节点看作“独立贡献”,只需一次扫描即可求得最优解。
算法思路2
-
计算差值数组
对每个节点 ,计算 -
求基础贡献
若不做任何操作,则所有节点价值之和为 -
统计正负差值
遍历所有 :- 若 ,累加到 ,并让 自增;同时更新正类最小值 。
- 否则(),更新负类最大值 。
-
计算不翻转奇偶(偶数个正差)时的总贡献
若此时 已经是偶数,则该值即为最优。
-
若 为奇数,需要翻转一次正负奇偶
需要从“所有 的集合”中去掉一个最小的,也就是或者向“”的集合中再添入一个最大的负值,即
令最终最优为
-
返回结果
返回上述最优值即可。
时间复杂度2
- 计算 数组需 。
- 统计正负、更新 , , , 也需一次 扫描。
- 整体时间复杂度为 ,空间复杂度为 (用于存储 数组)。
代码分解2
-
计算
1
deltas = [(num ^ k) - num for num in nums]
-
计算基础和
1
base_sum = sum(nums)
-
统计正负并维护最小正、最大负
1
2
3
4
5
6
7
8
9
10sumP, cntP = 0, 0
minP = float('inf')
maxN = -float('inf')
for delta in deltas:
if delta >= 0:
sumP += delta
cntP += 1
minP = min(minP, delta)
else:
maxN = max(maxN, delta) -
计算 “不翻转奇偶” 时的贡献
1
2contrib_same = base_sum + sumP
dp_root0 = contrib_same -
若 为奇数则做调整
1
2
3
4if cntP % 2 == 1:
drop_minP = base_sum + (sumP - minP) if cntP > 0 else -float('inf')
add_maxN = base_sum + (sumP + maxN) if maxN > -float('inf') else -float('inf')
dp_root0 = max(drop_minP, add_maxN) -
返回答案
1
return dp_root0
代码实现2
1 | from heapq import heappush, heappop |
| 维度 | 方法一:树形 DP | 方法二:全局贪心(基于差值) |
|---|---|---|
| 核心思想 | 在树上做自底向上的 DP,维护每个节点翻转与不翻转两种状态的最优值,合并子节点差值时需考虑“奇偶翻转” | 将所有节点视作“独立差值”,只需统计正差与负差的数量与极值,利用全局奇偶约束直接求解 |
| 约束来源 | 边选择会影响其相邻节点状态,需要在 DFS 中显式处理子树合并的“奇偶”关系 | 只需注意“被翻转节点数量必须为偶数”──一条全局约束,省去了树形结构的复杂递归 |
| 实现难度 | 需要写递归、合并子节点增益、维护正负差值的最小/最大值、考虑多种奇偶分支 | 只要一次扫描,代码简洁;无需建图或做 DFS |
| 时间复杂度 | ||
| 空间复杂度 | (邻接表 + DP 数组) | (差值数组 + 常数额外变量) |
| 典型场景 | 当关注对每条边操作的奇偶传递,并扩展到“若树结构更复杂”时可推广 | 若只需考虑“翻转约束 ”,且树是连通无环结构时最优 |
| 优势 | 能处理更一般的“子树方向性”DP,逻辑清晰;适合需要扩展到变体约束的情况 | 直接高效;只用一次扫描即可;易于理解与实现 |
| 劣势 | 代码复杂度高,容易出错;需要显式管理多种子情况与奇偶分支 | 只对“树”且仅含全局奇偶约束的特例适用;若约束更复杂则失效 |