您的位置:首页 > 编程语言 > C语言/C++

集训-打怪兽(树形DP)

2017-04-18 07:45 204 查看


打怪兽

题目描述

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

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

输入格式 1795.in

第一行,两个整数: N和STEP。 1 <= N <= 50, 1 <= STEP <= 100。

第二行,共N个整数,第i个整数是s[i]。 1 <= s[i] <= 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

很明显的树形DP。
我们知道最后可以不回到根节点。所以我们设一个状态dp[i][j][0]表示以i为根节点,走j步(到子树),最后不回到根节点可以杀死的最大小怪兽数。

然而,直接求这个dp[i][j][0]是非常困难的,在枚举i的儿子时,每增加一个新的儿子,状态很复杂,举个例子更好理解一点。



假设3就是新的儿子状态 dp[3][j][0]全部都算出来了,那状态的转移有两种,

情况1:就把之前所有儿子不回到根节点的状态(这里之前的所有儿子实际只有2这个节点)是,加上新儿子回到根节点的情况(因为你要有一种情况回到根节点,才能继续走啊)

情况2:就把之前所有儿子回到根节点的状态,加上新儿子不会回到根节的状态的情况。(理由同上)

可以发现转移都需要回到根节点的状态。

那么问题就变为如何求回到根节点的状态了。

dp[i][j][1]表示以i为根节点,走j步,也同样只在子树,但是最后要回到根节点的状态。

for(int j=step;j>=0;j--) 

4000
        for(int k=2;k<=j;k++)dp[root][j][1]=max(dp[root][j][1],dp[root][j-k][1]+dp[v][k-2][1]);

只要理解了上面的式子就差不多了。

j是枚举步数,要在旧的儿子这边走j-k步,最后回到根节点。加号右边的式子为什么k要-2呢?因为从root节点到他的儿子结点v,因为儿子这边是要回来根节点的,那么从根节点走到儿子结点,再从儿子结点回来根节点,这就消耗了两步了,所以说只剩下k-2步了。

还要注意一个小细节,j要从大的枚举到小的,因为新的状态不能重复计算新的儿子,所以这里是用滚动的,必须从大到小(不理解的等会看看代码可能就懂了)

既然dp[i][j][1]求出来了,那么dp[i][j][0]也可以求了。

        for(int j=step;j>=0;j--)
for(int k=1;k<=j;k++)
{
dp[root][j][0]=max(dp[root][j][0],dp[v][k-1][0]+dp[root][j-k][1]);
if(k>1)
dp[root][j][0]=max(dp[root][j][0],dp[v][k-2][1]+dp[root][j-k][0]);
}

第一个语句的意思是这样的:从之前的儿子的回到根节点状态走j-k步,然后再从跟根结点走k-1步(因为不用回来,但是还是要从根走到儿子需要1步)。

第二个语句的意思:从新的儿子走k步最后回来,再走j-k步不会来的。

注意:也用了滚动。

dp就完成了,最后的答案就是dp[1][step][0]。

还需要注意的:求dp[i][j][1]的这段代码必须写在dp[i][j][0]的后面,因为他们都是滚动,而dp[i][j][1]不依赖其他,但是dp[i][j][0]依赖dp[i][j][1],如果先算dp[i][j][1]那么会出现重复拿一个点的情况,详看代码。

code:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<vector>
using namespace std;
#define Maxn 102
#define Maxs 102
vector<int> f[Maxn];
int dp[Maxn][Maxs][2];
int n,step;
int s[Maxn];
void dfs(int root,int pre)
{
int len=f[root].size();
for(int i=0;i<len;i++)if(f[root][i]!=pre)
{
int v=f[root][i];
dfs(v,root);
for(int j=step;j>=0;j--)
for(int k=1;k<=j;k++)
{
dp[root][j][0]=max(dp[root][j][0],dp[v][k-1][0]+dp[root][j-k][1]);
if(k>1)
dp[root][j][0]=max(dp[root][j][0],dp[v][k-2][1]+dp[root][j-k][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[v][k-2][1]);
}
for(int i=0;i<=step;i++)
{
dp[root][i][0]+=s[root];
dp[root][i][1]+=s[root];
}
return;
}
int main()
{
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]);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
f[x].push_back(y);
f[y].push_back(x);
}
dfs(1,0);
printf("%d",dp[1][step][0]);
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  树形DP c语言 dp