06 树形DP:金明的预算方案

树形DP:金明的预算方案

其实这道题不用树形dp,也可以使用普通的背包dp,但是鉴于树形dp的简单题一题难求,于是我们用树形dp做。

P1064 [NOIP2006 提高组] 金明的预算方案

题目描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 nn 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件 附件
电脑 打印机,扫描仪
书柜 图书
书桌 台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 00 个、11 个或 22 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 nn 元。于是,他把每件物品规定了一个重要度,分为 55 等:用整数 151 \sim 5 表示,第 55 等最重要。他还从因特网上查到了每件物品的价格(都是 1010 元的整数倍)。他希望在不超过 nn 元的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 jj 件物品的价格为 vjv_j,重要度为wjw_j,共选中了 kk 件物品,编号依次为 j1,j2,,jkj_1,j_2,\dots,j_k,则所求的总和为:

vj1×wj1+vj2×wj2++vjk×wjkv_{j_1} \times w_{j_1}+v_{j_2} \times w_{j_2}+ \dots +v_{j_k} \times w_{j_k}

请你帮助金明设计一个满足要求的购物单。

输入格式

第一行有两个整数,分别表示总钱数 nn 和希望购买的物品个数 mm

22 到第 (m+1)(m + 1) 行,每行三个整数,第 (i+1)(i + 1) 行的整数 viv_ipip_iqiq_i 分别表示第 ii 件物品的价格、重要度以及它对应的的主件。如果 qi=0q_i=0,表示该物品本身是主件。

输出格式

输出一行一个整数表示答案。

样例 #1

样例输入 #1

1
2
3
4
5
6
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0

样例输出 #1

1
2200

提示

数据规模与约定

对于全部的测试点,保证 1n3.2×1041 \leq n \leq 3.2 \times 10^41m601 \leq m \leq 600vi1040 \leq v_i \leq 10^41pi51 \leq p_i \leq 50qim0 \leq q_i \leq m,答案不超过 2×1052 \times 10^5

定义状态:dp【i】【j】=以后序遍历的方式遍历到节点 i 时,还剩的空间为 j 时的最佳answer。

首先,我们需要构建一个树:(对于测试样例)注意一开始为了统摄所有根节点,我们建立一个0号节点来统摄。


这是这个dp数组在测试案例的情况下的最后输出结果,(后面可能会用到)

状态转移方程:xbcl

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
#include <bits/stdc++.h>
#define int long long
using namespace std;
vector<int> point_to[61];
int v[100]={0}, p[100]={0}, q[100]={0};
int dp[100][3200]={0};
int n, m;

void dfs(int nowpoint,int nowweight)
{//第一个参数表示当前节点,第二个参数表示当前还剩的重量
if(nowweight<=0)return ;//如果当前还剩的重量已经小于0,那么直接返回
for(int i=0;i<point_to[nowpoint].size();i++)
{//遍历每一个当前节点的子节点
int child = point_to[nowpoint][i];//child为子节点
for (int j = nowweight-v[child]; j >= 0; j--)
{
dp[child][j] = dp[nowpoint][j] + p[child]*v[child];
}
/*
把当前0~nowweight-v[child]的所有项给直接搬过来。
再加上p[child]*v[child]即题目规定的权值
其中0~nowweight-v[child]记录着遍历过他的哥哥节点(前面的兄弟节点)后,
所得出的最佳答案
为什么把0~nowweight-v[child]直接搬过来加?首先我们要知道
dp[父节点]【0~nowweight-v[child]】表示的是在父亲只有该子节点的所有哥哥节点的时候
,而完全不取该子节点的有关权值的时候的最优解。
因为我们现在在讨论该子节点取的情况下的最优解,
而取该子节点势必会消耗v[child]的限额,于是我们给该子节点预留v[child],
那么取完该节点后剩下0~nowweight-v[child],
在递归到叶子节点(即下一行程序的dfs)
返回后0~nowweight-v[child]一一顺序对应v[child]~nowweight
于是对应原来父节点集合的v[child]~nowweight元素比大小(下面一个循环干的事),
最终得到父节点nowpoint的最优值集合
*/
dfs(child, nowweight - v[child]);
for (int k = nowweight; k >= v[child]; k--)
{
dp[nowpoint][k] = max(dp[nowpoint][k], dp[child][k - v[child]]);
}
}
/*
这时候你会问了,对于兄弟节点之间,取两个,三个等等在哪里体现。
其实,这在搬过来加权值的时候已经体现了;
如处理样例中5号这个节点的时候,
我们把00000000···00000 1200 1200 1200···1200(十一(因为包括首尾)个)搬过来
在5号节点这个节点的权值1000加上去,dp第五行自然也就变成了1000 1000···1000 2200
2200··2200了
*/
}
signed main(void)
{
cin>>n>>m;
n/=10;
for(int i=1;i<=m;i++)
{
cin>>v[i]>>p[i]>>q[i];
v[i]/=10;
point_to[q[i]].push_back(i);
//建树的过程,point_to是二维数组,用point_to[i]记录i号节点指向的所有节点
}
dfs(0,n);

cout<<dp[0][n]*10<<endl;
return 0;
}

总结:

树状dp主要解决树形最优解问题。

步骤:

0.递归终止条件:

剩下限重小于0,返回即可;

1.遍历每一个子节点:

(1)搬来加权值:把父节点剪掉子节点的重量的部分照搬,加上子节点的权值,成为子节点的那一行的数组;

(2)递归:往下递归一层,就更新传参,更新当前节点的参数,更新当前限重。

(3)高位搬回去比较。