题目描述
有一棵 N 个结点的树,结点编号 1 至
N 。第 i 个结点有si 只怪兽。现在你要从第 1 个结点出发,最多走 step 步(每一步就是走一条边),当你到达一个结点时,你就可以把该结点的怪兽全部打死。现在问题是:在最优策略下,你最多可以打死多少只怪物?注意:可以多次经过同一个结点,但是该结点的怪物被打死后,该结点就没有怪物了。
输入格式 1795.in
第一行,两个整数: N 和
step 。 1≤N≤50 , 1≤step≤100 。第二行,共 N 个整数,第
i 个整数是 si 。 1≤si≤100 。接下来有 N−1 行,第 i 行是两个整数:
a 和 b ,表示结点a 和结点 b 之间有一条无向边。
输出格式 1795.out
一个整数。
输入样例 1795.in
输入样例一:
2 6
7 1
1 2
输入样例二:
3 5
2 3 9
3 2
1 2
输入样例三:
5 3
6 1 6 4 4
1 4
2 4
5 1
3 4
输入样例四:
10 4
4 2 1 6 3 7 8 5 2 9
9 6
1 6
7 9
10 6
5 6
8 1
3 1
4 6
2 6
输入样例五:
50 48
6 9 4 9 5 8 6 4 4 1 4 8 3 4 5 8 5 6 4 9 7 9 7 9 5 2 7 2 7 7 5 9 5 8 5 7 1 9 3 9 3 6 4 5 5 4 7 9 2 2
25 5
22 5
35 25
42 25
40 25
9 42
32 25
12 40
37 5
44 35
23 25
1 32
24 42
28 9
20 32
4 23
26 40
33 25
11 20
48 33
34 26
6 37
16 12
50 1
46 48
17 24
8 22
43 25
18 11
30 24
31 48
36 34
39 18
13 9
10 50
45 42
3 16
47 40
15 1
2 10
29 47
19 22
7 48
14 44
41 48
49 1
38 4
27 46
21 47
输出样例 1795.out
输出样例一:
8
输出样例二:
14
输出样例三:
16
输出样例四:
26
输出样例五:
194
这题可能会感觉跟之前的蚂蚁聚会有点像,但是又不太一样。看起来这题固定了出发点,可能会简单一些,其实不然。
题意:有一棵树,每个结点有权值
做法一:暴力
从根结点开始做 DFS,多于
step
步或到达叶子结点则停止搜索,即可得解。
得分:46。
代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
const int maxn = 100;
int n, step, ans;
int s[maxn];
int head[maxn];
struct Edge { int to, next; } edge[maxn];
int cnt = 0;
void addEdge(int u, int v) {
edge[++cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt;
}
void dfs(int now, int k, int sum) {
ans = max(ans, sum);
if (k == step) return;
for (int i = head[now]; i; i = edge[i].next) {
int t = s[edge[i].to];
s[edge[i].to] = 0;
dfs(edge[i].to, k + 1, sum + t);
s[edge[i].to] = t;
}
}
int main(void) {
freopen("1795.in", "r", stdin);
freopen("1795.out", "w", stdout);
scanf("%d%d", &n, &step);
for (int i = 1; i <= n; i++) scanf("%d", &s[i]);
memset(head, 0, sizeof head);
for (int i = 1; i < n; i++) {
int a, b;
scanf("%d%d", &a, &b);
addEdge(a, b); addEdge(b, a);
}
int t = s[1]; s[1] = 0;
ans = 0;
dfs(1, 0, t);
printf("%d\n", ans);
return 0;
}
做法二:树型 DP
其实考试的时候我是想过树型 DP 的,但是感觉条件太多了,我记的状态是
f[i][j][k][0/1]
表示从第
i
个结点出发,最多走
lgj 说过,想问题不要一下子想那么复杂。我们来把状态改进一下试试看。
首先,当前树的根结点
i
是必须保存的了,而所走步数显然也是一个不可避免的参数。但是,最后停留在哪个结点,需要记吗?根结点有无怪物呢?
我们先来回答后一个,根结点有无怪物显然是不用记的。既然是最优方案,那么我们在到达根结点的时候就应该立即杀掉所在的怪物。
那么,最后停留在哪个结点是否要记呢?
其实也是不用的。能够影响我们决策的关键并不在于最后在具体的哪个结点,而是是否回到根结点。
为什么我们不用纠结于具体停留的结点呢?因为我们的问题是受步数限制的,不管它最后停留在哪个儿子(或者孙子,etc.),至少我们知道它是从当前问题的根结点走了若干步到那里的,这就够了。步数可以帮助父亲在考虑当前子树时做决策,而跟停留的具体结点没有半毛钱关系。
这是决定状态的关键,读到这里请停下来仔细品味一下,想明白再往下看。
那么我们不妨设
如果从左往右依次考虑每棵子树,不回根结点的情况可能由哪些子问题推导而来呢?
有两种情况。
第一种:将当前子树作为最后停留的子树。既然最后是不回到根结点的,那么一定停留在某个子树,也就是说要取某个同样是不回根结点的子问题,而在原树剩余的子树中,肯定是兜兜转转在一些子树中下去了,又或者某些子树根本没有被访问过。但无论如何,最终(在最后一次下子树前)一定会回到根结点。
如果从原树的根结点出发走
注意为什么上面转移方程中从子树的根结点开始只能走
j−1
步呢?因为从根结点下到子结点本身要消耗一步嘛,而且不回来。
事实上,跟前面见过的几道树型 DP 结合背包一样,这里也可以通过改变循环顺序,省掉滚动。(下面的式子就直接统一写
f
了)
第二种:曾经经过当前子树,但是不是最后停留在当前子树。那么就有
不难想到,从子树的根结点开始只能走 j−2 步是因为当前子树只是作为中转,最后是要回到根结点再最终停留在别的子树的。那么途中从根结点到当前子树根结点一来一回,消耗两步。
现在问题就变成了如何求
f[root][i][1]
了。
这个只有一种情况,就是选一些子树走若干步,且都要回到子树的根结点。还是按照我们每个儿子依次考虑的习惯,那么就有
有了之前的基础应该就比较好理解了, j−2 与上面情况二的道理是一样的。
而且千万要记得最后考虑完所有子树之后得到的 0 和 1 的情况都要加上 si 。其实它是最先取的,但是因为考虑儿子的时候要取 max 不方便,所以放到最后再来加。
最终所求的答案就是
f[1][step][0]
。
但是要注意一个问题,在考虑同一个子树的时候,应该先更新
f[root][i][0]
再更新
f[root][i][1]
,因为 0 的情况是要靠 1 来计算的,避免重复取同一个点。
时间复杂度: O(n×step2) 。
得分:100。
参考代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
const int maxn = 50 + 10;
const int maxstep = 100 + 10;
int n, step;
int s[maxn];
int dp[maxn][maxstep][5];
int head[maxn];
struct Edge { int to, next; } edge[maxn << 1];
int cntEdge = 0;
void addEdge(int u, int v) {
edge[++cntEdge].to = v;
edge[cntEdge].next = head[u];
head[u] = cntEdge;
}
void dfs(int root, int pre) {
for (int i = head[root]; i; i = edge[i].next)
if (edge[i].to != pre) {
int son = edge[i].to;
dfs(son, root);
for (int j = step; j >= 0; j--) //从大到小循环滚动
for (int k = 1; k <= j; k++) {
//当前儿子不作为最终停留
if (k > 1) dp[root][j][0] = max(dp[root][j][0], dp[root][j - k][0] + dp[son][k - 2][1]);
//当前儿子作为最终停留
dp[root][j][0] = max(dp[root][j][0], dp[root][j - k][1] + dp[son][k - 1][0]);
}
for (int j = step; j >= 0; j--)
for (int k = 2; k <= j; k++) //经过当前儿子,但最终回到根结点
dp[root][j][1] = max(dp[root][j][1], dp[root][j - k][1] + dp[son][k - 2][1]);
}
for (int i = 0; i <= step; i++) { //记得加上根结点本身的权值
dp[root][i][0] += s[root];
dp[root][i][1] += s[root];
}
}
int main(void) {
freopen("1795.in", "r", stdin);
freopen("1795.out", "w", stdout);
scanf("%d%d", &n, &step);
for (int i = 1; i <= n; i++) scanf("%d", &s[i]);
memset(head, 0, sizeof head);
for (int i = 1; i < n; i++) {
int a, b;
scanf("%d%d", &a, &b);
addEdge(a, b); addEdge(b, a);
}
memset(dp, 0, sizeof dp);
dfs(1, 0);
printf("%d\n", dp[1][step][0]);
return 0;
}