0%

2920. 收集所有金币可获得的最大积分

2920.收集所有金币可获得的最大积分

题目描述:

有一棵由 n 个节点组成的无向树,以 0 为根节点,节点编号从 0n - 1 。给你一个长度为 n - 1 的二维 整数 数组 edges ,其中 edges[i] = [ai, bi] 表示在树上的节点 aibi 之间存在一条边。另给你一个下标从 0 开始、长度为 n 的数组 coins 和一个整数 k ,其中 coins[i] 表示节点 i 处的金币数量。

从根节点开始,你必须收集所有金币。要想收集节点上的金币,必须先收集该节点的祖先节点上的金币。

节点 i 上的金币可以用下述方法之一进行收集:

  • 收集所有金币,得到共计 coins[i] - k 点积分。如果 coins[i] - k 是负数,你将会失去 abs(coins[i] - k) 点积分。
  • 收集所有金币,得到共计 floor(coins[i] / 2) 点积分。如果采用这种方法,节点 i 子树中所有节点 j 的金币数 coins[j] 将会减少至 floor(coins[j] / 2)

返回收集 所有 树节点的金币之后可以获得的最大积分。

数据范围:

$2 \le n \le 10^5; 0 \le coins[i] \le 10^4; 0 \le k \le 10^4$

题解:

题目看错了,写了个递推的树形dp,结果答案不对。后来才发现采用第二种方法的话,节点 $i$ 子树中的所有节点,不只是直系儿子节点,金币都会减少至 $floor(coins[j] / 2)$ ,也就是所有的节点都要除以 $2$ 。

因为 $0 \le coins[u] \le 10^4$ ,数据范围比较小,可以发现最多除以 $2$ 出现 $14$ 次,以后子树中所有节点的值全是 $0$ 。

可以使用记忆化搜索,记录路径上已经使用了几次方式二,大于等于 $14$ 时直接返回。否则的话对 $u$ 节点枚举使用了方式一还是方式二。

代码:

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
40
41
42
43
44
45
46
47
48
49
auto optimize_cpp_stdio = []()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
return 0;
}();
class Solution
{
public:
const static int maxn = 1e5 + 10;
const static int maxm = 1e5 + 10;
const int INF = 0x3f3f3f3f;
vector<vector<int>> g;
vector<vector<int>> dp; // dp[u][j] 表示子树 u,上面有j个除2,最大分数
int dfs(int u, int fa, int cnt, vector<int> &coins, int k)
{
if (cnt >= 14)
return 0;
if (dp[u][cnt] != -1)
return dp[u][cnt];
int tmp1 = floor(coins[u] >> cnt) - k;
int tmp2 = floor(coins[u] >> (cnt + 1));
for (int i = 0; i < g[u].size(); ++i)
{
int v = g[u][i];
if (v == fa)
continue;
tmp1 += dfs(v, u, cnt, coins, k);
tmp2 += dfs(v, u, cnt + 1, coins, k);
}
dp[u][cnt] = max(tmp1, tmp2);
return dp[u][cnt];
}
int maximumPoints(vector<vector<int>> &edges, vector<int> &coins, int k)
{
int n = coins.size();
g.resize(n);
dp.resize(n, vector<int>(14, -1));
for (auto &edge : edges)
{
int u = edge[0];
int v = edge[1];
g[u].emplace_back(v);
g[v].emplace_back(u);
}
return dfs(0, -1, 0, coins, k);
}
};