[SMOJ1795]打怪兽

97 篇文章 0 订阅
11 篇文章 0 订阅
本文介绍了一道关于树形DP的题目,题目要求从树的根节点出发,最多走step步,每到达一个节点可以打死该节点的所有怪物,目标是找出最多可以打死多少只怪物。给出了两种解题思路:暴力DFS和树形DP。通过简化状态转移方程,最终状态定义为以i为根节点,走j步,最后不回到根节点的最大怪物数,以及回到根节点的最大怪物数。通过递归或迭代方式计算这些状态,得到答案。
摘要由CSDN通过智能技术生成

题目描述

有一棵 N 个结点的树,结点编号 1 至 N。第 i 个结点有 si 只怪兽。现在你要从第 1 个结点出发,最多走 step 步(每一步就是走一条边),当你到达一个结点时,你就可以把该结点的怪兽全部打死。

现在问题是:在最优策略下,你最多可以打死多少只怪物?注意:可以多次经过同一个结点,但是该结点的怪物被打死后,该结点就没有怪物了。

输入格式 1795.in

第一行,两个整数: N step 1N50 , 1step100

第二行,共 N 个整数,第 i 个整数是 si 1si100

接下来有 N1 行,第 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


这题可能会感觉跟之前的蚂蚁聚会有点像,但是又不太一样。看起来这题固定了出发点,可能会简单一些,其实不然。

题意:有一棵树,每个结点有权值 vi,从根结点出发走 step 步,第一次到达某个结点时得到该结点的权值,求最多能够获得的权值之和。

做法一:暴力
从根结点开始做 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 个结点出发,最多走 j 步,最后停留在 k ,结点 i 有无怪物,最多能打死的怪物数。其实完全不需要。

lgj 说过,想问题不要一下子想那么复杂。我们来把状态改进一下试试看。

首先,当前树的根结点 i 是必须保存的了,而所走步数显然也是一个不可避免的参数。但是,最后停留在哪个结点,需要记吗?根结点有无怪物呢?
我们先来回答后一个,根结点有无怪物显然是不用记的。既然是最优方案,那么我们在到达根结点的时候就应该立即杀掉所在的怪物。

那么,最后停留在哪个结点是否要记呢?
其实也是不用的。能够影响我们决策的关键并不在于最后在具体的哪个结点,而是是否回到根结点
为什么我们不用纠结于具体停留的结点呢?因为我们的问题是受步数限制的,不管它最后停留在哪个儿子(或者孙子,etc.),至少我们知道它是从当前问题的根结点走了若干步到那里的,这就够了。步数可以帮助父亲在考虑当前子树时做决策,而跟停留的具体结点没有半毛钱关系。

这是决定状态的关键,读到这里请停下来仔细品味一下,想明白再往下看。

那么我们不妨设 f[i][j][0] 表示以 i 为根节点,从 i 开始走 j 步,最后不回到根节点可以杀死的最大怪物数,而最后一维为 1 则表示回到根结点。
如果从左往右依次考虑每棵子树,不回根结点的情况可能由哪些子问题推导而来呢?
有两种情况。

第一种:将当前子树作为最后停留的子树。既然最后是不回到根结点的,那么一定停留在某个子树,也就是说要取某个同样是不回根结点的子问题,而在原树剩余的子树中,肯定是兜兜转转在一些子树中下去了,又或者某些子树根本没有被访问过。但无论如何,最终(在最后一次下子树前)一定会回到根结点。
如果从原树的根结点出发走 i 步,其中有 j 步是关于最后停留的子树中的,那么就有

g[root][i][0]=max{f[root][i][0],f[son][j1][0]+f[root][ij][1])}

注意为什么上面转移方程中从子树的根结点开始只能走 j1 步呢?因为从根结点下到子结点本身要消耗一步嘛,而且不回来。
事实上,跟前面见过的几道树型 DP 结合背包一样,这里也可以通过改变循环顺序,省掉滚动。(下面的式子就直接统一写 f 了)

第二种:曾经经过当前子树,但是不是最后停留在当前子树。那么就有

f[root][i][0]=max{f[root][i][0],f[son][j2][1]+f[root][ij][0]}

不难想到,从子树的根结点开始只能走 j2 步是因为当前子树只是作为中转,最后是要回到根结点再最终停留在别的子树的。那么途中从根结点到当前子树根结点一来一回,消耗两步。

现在问题就变成了如何求 f[root][i][1] 了。
这个只有一种情况,就是选一些子树走若干步,且都要回到子树的根结点。还是按照我们每个儿子依次考虑的习惯,那么就有

f[root][i][1]=max{f[root][i][1],f[root][ij][1]+f[son][j2][1]}

有了之前的基础应该就比较好理解了, j2 与上面情况二的道理是一样的。

而且千万要记得最后考虑完所有子树之后得到的 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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值