Hard
题目描述
存在一个以节点 0 为根的 n 个节点的无向树,节点标记为 0 到 n - 1。给定一个长度为 n - 1 的二维整数数组 edges,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间有一条边。还给定一个大小为 n 的 0 索引数组 coins,其中 coins[i] 表示顶点 i 中的硬币数量,以及一个整数 k。
从根节点开始,你必须收集所有硬币,使得只有在已经收集了节点祖先的硬币后,才能收集该节点的硬币。
节点 i 的硬币可以通过以下方式之一收集:
- 收集所有硬币,但你将获得 coins[i] - k 分。如果 coins[i] - k 为负数,则你将失去 abs(coins[i] - k) 分。
- 收集所有硬币,但你将获得 floor(coins[i] / 2) 分。如果使用这种方式,那么对于节点 i 子树中存在的所有节点 j,coins[j] 将减少到 floor(coins[j] / 2)。
返回从所有树节点收集硬币后可以获得的最大分数。
示例 1:
输入:edges = [[0,1],[1,2],[2,3]], coins = [10,10,3,3], k = 5
输出:11
解释:
使用第一种方式从节点 0 收集所有硬币。总分 = 10 - 5 = 5。
使用第一种方式从节点 1 收集所有硬币。总分 = 5 + (10 - 5) = 10。
使用第二种方式从节点 2 收集所有硬币,所以节点 3 剩余的硬币为 floor(3 / 2) = 1。总分 = 10 + floor(3 / 2) = 11。
使用第二种方式从节点 3 收集所有硬币。总分 = 11 + floor(1 / 2) = 11。
可以证明从所有节点收集硬币后能获得的最大分数是 11。
示例 2:
输入:edges = [[0,1],[0,2]], coins = [8,4,4], k = 0
输出:16
解释:
所有节点的硬币都使用第一种方式收集。因此,总分 = (8 - 0) + (4 - 0) + (4 - 0) = 16。
提示:
- n == coins.length
- 2 <= n <= 10^5
- 0 <= coins[i] <= 10^4
- edges.length == n - 1
- 0 <= edges[i][0], edges[i][1] < n
- 0 <= k <= 10^4
解题思路
这是一个经典的树形动态规划问题。关键在于理解两种收集方式的影响:
核心思路:
状态定义:设
dp[x][t]表示从节点 x 的子树中能获得的最大分数,其中 t 表示该节点的祖先中使用第二种方式(除以2)的次数。状态转移:对于每个节点,我们有两种选择:
- 使用第一种方式:获得
(coins[x] >> t) - k分,子节点的除以2次数保持为 t - 使用第二种方式:获得
(coins[x] >> (t+1))分,子节点的除以2次数变为 t+1
- 使用第一种方式:获得
剪枝优化:当 t >= 14 时,由于 coins[i] <= 10^4,此时
coins[i] >> 14必定为 0,可以直接返回 0。记忆化搜索:使用记忆化避免重复计算,提高效率。
状态转移方程:
dp[x][t] = max(
(coins[x] >> t) - k + sum(dp[child][t]),
(coins[x] >> (t+1)) + sum(dp[child][t+1])
)
代码实现
class Solution {
public:
int maximumPoints(vector<vector<int>>& edges, vector<int>& coins, int k) {
int n = coins.size();
vector<vector<int>> graph(n);
for (const auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
}
vector<int> parent(n, -2);
vector<int> order = {0};
parent[0] = -1;
for (int index = 0; index < n; index++) {
int node = order[index];
for (int child : graph[node]) {
if (child == parent[node]) continue;
parent[child] = node;
order.push_back(child);
}
}
vector<vector<int>> dp(n, vector<int>(15));
for (int index = n - 1; index >= 0; index--) {
int node = order[index];
for (int shifts = 13; shifts >= 0; shifts--) {
int keep = (coins[node] >> shifts) - k;
int halve = coins[node] >> (shifts + 1);
for (int child : graph[node]) {
if (parent[child] != node) continue;
keep += dp[child][shifts];
halve += dp[child][shifts + 1];
}
dp[node][shifts] = max(keep, halve);
}
}
return dp[0][0];
}
};
class Solution:
def maximumPoints(self, edges: List[List[int]], coins: List[int], k: int) -> int:
n = len(coins)
graph = [[] for _ in range(n)]
for a, b in edges:
graph[a].append(b)
graph[b].append(a)
parent = [-2] * n
parent[0] = -1
order = [0]
for node in order:
for child in graph[node]:
if child == parent[node]:
continue
parent[child] = node
order.append(child)
dp = [[0] * 15 for _ in range(n)]
for node in reversed(order):
for shifts in range(13, -1, -1):
keep = (coins[node] >> shifts) - k
halve = coins[node] >> (shifts + 1)
for child in graph[node]:
if parent[child] != node:
continue
keep += dp[child][shifts]
halve += dp[child][shifts + 1]
dp[node][shifts] = max(keep, halve)
return dp[0][0]
public class Solution {
public int MaximumPoints(int[][] edges, int[] coins, int k) {
int n = coins.Length;
var graph = new List<List<int>>();
for (int i = 0; i < n; i++) {
graph.Add(new List<int>());
}
foreach (int[] edge in edges) {
graph[edge[0]].Add(edge[1]);
graph[edge[1]].Add(edge[0]);
}
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = -2;
parent[0] = -1;
var order = new List<int> {0};
for (int index = 0; index < n; index++) {
int node = order[index];
foreach (int child in graph[node]) {
if (child == parent[node]) continue;
parent[child] = node;
order.Add(child);
}
}
int[][] dp = new int[n][];
for (int i = 0; i < n; i++) dp[i] = new int[15];
for (int index = n - 1; index >= 0; index--) {
int node = order[index];
for (int shifts = 13; shifts >= 0; shifts--) {
int keep = (coins[node] >> shifts) - k;
int halve = coins[node] >> (shifts + 1);
foreach (int child in graph[node]) {
if (parent[child] != node) continue;
keep += dp[child][shifts];
halve += dp[child][shifts + 1];
}
dp[node][shifts] = Math.Max(keep, halve);
}
}
return dp[0][0];
}
}
var maximumPoints = function(edges, coins, k) {
const n = coins.length;
const graph = Array.from({length: n}, () => []);
for (const [a, b] of edges) {
graph[a].push(b);
graph[b].push(a);
}
const parent = new Int32Array(n);
parent.fill(-2);
parent[0] = -1;
const order = [0];
for (let index = 0; index < order.length; index++) {
const node = order[index];
for (const child of graph[node]) {
if (child === parent[node]) continue;
parent[child] = node;
order.push(child);
}
}
const dp = Array.from({length: n}, () => new Int32Array(15));
for (let index = n - 1; index >= 0; index--) {
const node = order[index];
for (let shifts = 13; shifts >= 0; shifts--) {
let keep = (coins[node] >> shifts) - k;
let halve = coins[node] >> (shifts + 1);
for (const child of graph[node]) {
if (parent[child] !== node) continue;
keep += dp[child][shifts];
halve += dp[child][shifts + 1];
}
dp[node][shifts] = Math.max(keep, halve);
}
}
return dp[0][0];
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(n × log(max(coins))) |
| 空间复杂度 | O(n × log(max(coins))) |
说明:
- 时间复杂度:每个节点最多被访问 14 次(因为 log₂(10⁴) ≈ 14),所以总时间复杂度为 O(n × 14) = O(n)
- 空间复杂度:邻接表、显式遍历顺序和 DP 数组均为 O(n),不再依赖递归栈