【题解】洛谷 P4037 [JSOI2008] 魔兽地图 [树上背包]
题面:P4037 [JSOI2008] 魔兽地图 - 洛谷
乍一看挺唬人,但核心思想就是有两种不同的武器,
都可以用钱买,都有力量值,都有数量限制。
一种高端的能用低端的合成,低端的只能买。
问给你固定的金币数,最多能提升多少武力值?
解析:
题目都暗示你了,肯定是树上背包,但不好想。
一步步的思考,对于每一种武器:
如果是低端武器,那么分成两部分,一部分留给合并高端武器,一部分转化为武力值。
如果是高端武器,先递归所有合成需要的低端武器。
枚举对于每一个低端武器使用的金币数,再合并。
除了最高端的武器,其他高端武器也都要留一部分给更高端的武器合并。
那这样初步的状态构造就有了:
f[x][i][j]表示处理武器 x 时,预留 i 个 x 用于合成更高级装备,
最后花费 j 金币能获得的最大力量值
p[N]: 装备提供的力量值
a[N]: 基本装备的单价,高级装备是合成所需的总金币
n 是装备的种类数,m 是金币数
更细分的说一下,如果是最低端的武器 x:
用数组 sum 存储这个武器最多可以买多少个,sum[x] = m / a[x]。
枚举 i 为合成武器 x 的数量 (i <= sum[x]),j 为转化为力量值的数量 (j <= i):
如果是高级装备 x,枚举每一个要用到的低级装备 y,递归去跑一遍。
合成 x 所需 y 数量)
顺便处理下 a[x],。
枚举总的合成的 x 数量 i (i <= sum[x]),再枚举每一个 y。
把金币分成两部分,一部分花在当前 y 节点,另一部分花在前面枚举的其它 y 节点。
开个数组 g 记录当前 x 花 k 金币能得到的最多力量值。
枚举 ga 为当前 x 武器总使用金币数,gb 为分配给当前 y 的金币数。
更新当前 g[ga] 时就用:
合成 x 所需 y 数量
g[ga -gb]: 花费 ga - gb 金币在之前的 y 上能获得的最大力量值
那一坨 f 是因为我们要预留 i *合成 x 所需 y 数量个 y,因为你要 i 个 x,花费 gb 个金币。
总合并时,枚举预留的 x 的数量 j (j <= i),再枚举花的金币 k,用 g[k] + p[x] * (i - j) 更新 f[x][j][k]。
最高级的武器还得单独处理,详见主函数。
有很多注释的代码:
(可以跟着我上面的思路打代码)
#include<bits/stdc++.h>
using namespace std;const int N = 55, M = 2010;
//2 * N 多开点,之后会有莫名其妙的东西炸掉
int f[N][2 * N][M]; //f[x][i][j]表示处理武器 x 时,预留 i 个 x 用于合成更高级装备,//最后花费 j 金币能获得的最大力量值
int p[N], a[N]; //p[N]: 装备提供的力量//a[N]: 基本装备的单价,高级装备是合成所需的总金币
int sum[N]; //sum[x]:武器 x最多能购买的数量
int ans[M]; //最后答案存储
int n, m;struct node {int y, num;
} ;
vector<node> G[N]; //我用 vector 存武器之间的依赖关系
bool v[N]; // v[x]:1 代表 x 不是最高端的武器,反之 0 代表是
bool b[N]; // b[x]:1 代表 x 已经 dfs 过,反之 0 代表还没有
int g[M]; //临时最大值存储 void dfs(int x) {if (b[x]) { //节省时间 return ;}b[x] = 1;if (G[x].size() == 0) { //如果是最低端的武器 sum[x] = min(sum[x], m / a[x]);for (int i = 0; i <= sum[x]; i++) { //合成武器 x 的数量for (int j = 0; j <= i; j++) { //转化为力量值的数量f[x][i - j][i * a[x]] = p[x] * j;}}return ; //处理完了就退出 }sum[x] = 1e9; //初始化最大值 for (auto i: G[x]) {dfs(i.y); //递归 y sum[x] = min(sum[x], sum[i.y] / i.num);a[x] += i.num * a[i.y]; //累计花费值 }sum[x] = min(sum[x], m / a[x]);for (int i = 0; i <= sum[x]; i++) { // i: 总合成 x 数量int g[M];memset(g, -0x3f, sizeof(g)); g[0] = 0;//为什么负无穷?见主函数 f 初始化 for (auto j: G[x]) {for (int ga = m; ga >= 0; ga--) { //当前 x 武器总使用金币数 int t = -1e9; //最大值初始化//为什么负无穷?见主函数 f 初始化 for (int gb = 0; gb <= ga; gb ++) { //分配给当前 y 的金币数 t = max(t, g[ga - gb] + f[j.y][i * j.num][gb]);//g[ga -gb]: 花费 ga - gb 金币在之前的 y 上能获得的最大力量值//f[j.y][i * j.num][gb]:预留 i * j.num个 y,因为你要 i 个 x,花费 gb 个金币 //关于 ga 为什么要倒着遍历,因为我们不能取当前 y 武器更新过的 g[ga - gb]//只能使用上一个合成 x 需要的低端武器的 gg[ga] = t; //更新当前 g }}}//全部 y 都递归完了,合并 for (int j = 0; j <= i; j++) { //预留的 x 的数量for (int k = 0; k <= m; k++) { //总共花的金币f[x][j][k] = max(f[x][j][k], g[k] + p[x] * (i - j));}} }//自己想想为什么按照这个顺序枚举更新,//for (int i = 0; i <= sum[x]; i++) 和 for (auto j: G[x])可以调换位置吗?
} int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n >> m;memset(v, 0, sizeof(v));for (int i = 1; i <= n; i++) {cin >> p[i];char s[5]; //得开多点,防止奇怪的空格 cin >> s;if (s[0] == 'A') {int C;cin >> C;for (int j = 1; j <= C; j++) {node mt;cin >> mt.y >> mt.num;v[mt.y] = 1;G[i].push_back(mt);} }else {cin >> a[i] >> sum[i];}}memset(f, -0x3f, sizeof(f));//为什么要负无穷?因为如果 f[i][j][k] 中间的 j 是你真的要预留 k 个才可以合法的//毕竟之后递归的时候要用到。 memset(ans, 0, sizeof(ans)); //为什么都是 0?你最少也就只能拥有 0 个力量for (int i = 1; i <= n; i++) if(!v[i]) { //如果是最高端的武器 dfs(i); //先递归处理 for (int j = m; j >= 0; j--) { //总花费金币数量 for (int k = 0; k <= j; k++) { //真正给 i 使用的金币数量 ans[j] = max(ans[j], ans[j - k] + f[i][0][k]);// f[i][0][k]表示不预留 i,花费 k 金币能获得的最大力量值//关于这里为什么 j 是倒着枚举的,因为我们不能取当前 i 武器更新过的 ans[j - k]//只能使用上一个最高端武器的 ans }}}cout << ans[m] << "\n";return 0;
}