Leetcode 2603. 收集树中的金币

(5 mins to read)

题意

给定一棵$n$个点的无向无根树,每个点有或没有金币。

现在让你从某个节点出发,每次可以收集距离当前点2以内的所有金币,或者移动到一个相邻节点。

问你收集所有金币并返回到出发点最少的步数。

做法

首先考虑固定出发点的情况,答案是什么。显然就是去掉所有叶子节点距离小于等于2的边后剩余边的个数乘以2。

换根DP明显可以解决。

然后我写了1个小时,调了30分钟(实在想不到问题在哪,手画了一下十几个点的错误数据才意识到问题)。

代码写的也是异常丑陋,懒得在递归函数里传一堆参数,所以全改成类的私有变量了。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Solution {
public:
int collectTheCoins(vector<int>& coins, vector<vector<int>>& edges) {
n = coins.size();
dp.resize(0);
dp.resize(n);
dp2.resize(0);
dp2.resize(n);
G.resize(0);
G.resize(n);
c = coins;
for (const auto& e : edges) {
G[e[0]].push_back(e[1]);
G[e[1]].push_back(e[0]);
}
dfs(0, -1);
dfs2(0, -1);
int ans = 2 * n;
for (int i = 0; i < n; i++) ans = min(ans, dp[i][3] + dp2[i][3]);
return ans;
}
private:
void dfs(int u, int fa) {
for (int v : G[u]) {
if (v == fa) continue;
dfs(v, u);
if (c[v]) dp[u][0]++;
dp[u][1] += dp[v][0];
dp[u][2] += dp[v][1] + dp[v][2];
if (dp[v][1] || dp[v][2]) {
dp[u][3] += dp[v][3] + 2;
}
}
}
void dfs2(int u, int fa) {
for (int v : G[u]) {
if (v == fa) continue;
if (c[u]) dp2[v][0]++;
dp2[v][1] += dp2[u][0] + dp[u][0] - c[v];
dp2[v][2] += dp2[u][1] + dp2[u][2] + dp[u][1] + dp[u][2] - dp[v][0] - dp[v][1] - dp[v][2];
// 一开始错就是因为没加父节点u的子树内的部分,比如父节点有一个有金币的儿子,它只被统计在dp[u][0]中,
// 但正是由于这个金币,父节点的另一个儿子的儿子就需要往父亲走了
bool ok = false;
if (dp2[u][1] || dp2[u][2] || dp2[u][3]) {
dp2[v][3] += dp2[u][3];
ok = true;
}
if (dp[u][1] + dp[u][2] - dp[v][0] - dp[v][1] - dp[v][2] > 0) {
dp2[v][3] += dp[u][3];
if (dp[v][1] || dp[v][2]) {
dp2[v][3] -= dp[v][3] - 2;
}
ok = true;
}
if (ok) dp2[v][3] += 2;
}
for (int v : G[u]) {
if (v == fa) continue;
dfs2(v, u);
}
}
int n;
vector<array<int, 4>> dp, dp2;
vector<int> c;
vector<vector<int>> G;
};

正解

拓扑排序。

对于树中的叶子节点,如果没有金币,直接删除即可。

不断删除后,现在的树的所有叶子节点都有金币,显然我们可以继续删掉两层叶子节点。

答案就是剩下的边数乘以2了。其实就是贪心,并不需要算出从每个点出发的答案具体是什么。