HDU - 2639 Bone Collector II 题解
题目大意
一个人收藏骨头,有 n 个骨头,每个骨头有体积和价值,问能够装在容量为 V 的背包中,能获得的第 k 大(去重后)价值是多少。
样例
样例输入 1
5 10 2
1 2 3 4 5
5 4 3 2 1
样例输出 1
12
样例输入 2
5 10 12
1 2 3 4 5
5 4 3 2 1
样例输出 2
2
分析
- 跑暴力显然不优秀,每种物品可选可不选,最多 \(2^n\) 种不同的方案,也就对应这么多价值,显然如果都存下再排序输出结果,简单了事,但是显然时间和空间都承受不住。
- 当然在跑背包的时候,我们每次的决策都是在前面的基础上求得当前阶段每个状态的最优解。如果单纯的想最后把数组中所有的值都拿来排序,也不可行。整个转移过程并不能保证比最优解小一点的就是次大值。
- 我们可以针对每个容量 j,同时维护 k 个值,来保存放到容量 j 的时候前 k 大价值。在此基础上我们再考虑针对第 i 件物品的转移:
- 定义 \(f[i][j][k]\) 表示前 i 件物品放到容量为 j 背包中,获得的第 k 大价值,由于第 i 捡物品状态转移的时候只跟前 i-1 的状态有关,因此还可以简化成二维,即 \(f[j][k]\)。
- 放与不放第 i 件物品的最大值从何而来?很显然,就是普通的转移方程的两个值取较大者:\(max\{f[j][1], f[j-vi][1]+wi\}\),即放与不放分别有一个最大值,从二者当中选取一个作为最大。
- 继续考虑第二大的候选值有哪些?
- 有可能该物品性价比极其低,那显然不选它可能是最好的,这样有可能最大值和次大值都为不放该物品对应的值;
- 有可能该物品的性价比超高,那显然选择它可能是最好的,这样有可能最大值和次大值都为放下该物品对应的值;
- 因此次大值的候选值会有 4 个:\(f[j][1], f[j][2], f[j-vi][1]+wi, f[j-vi][2]+wi\);
- 以此类推,针对每个容量,都会有 2k 个候选值。如果 \(f[j]\) 和 \(f[j-vi]\) 都存在 k 个不同值,那么 当前的 \(f[j]\) 肯定能够从中得到 k 个不同的值。
- 另外一个问题就是如何选取 k 个不同值:
- 可以 sort 一遍再去重,简单粗暴有效
- 节俭归并排序中的合并,因为 \(f[j][k]\) 和 \(f[j-vi][k]+wi\) 都分别是降序序列,因此搞两个数组,从中不断取数合并在一起就可以了,代码略长,但是效率高不少
代码(归并的方法)
// 归并排序法合并两部分,取前kth个数
void bag() {
for (int i = 1; i <= n; ++i) {
for (int j = vol; j >= cost[i]; --j) {
int a[K] = {}, b[K] = {};
for (int k = 1; k <= kth; ++k) {
a[k] = dp[j][k];
b[k] = dp[j-cost[i]][k]+val[i];
}
a[kth+1] = -1;
b[kth+1] = -1;
// 合并
int k = 1, x = 1, y = 1;
while (k <= kth && (x <= kth || y <= kth)) {
dp[j][k] = a[x] > b[y] ? a[x++] : b[y++];
if (dp[j][k] != dp[j][k-1]) {
k++;
}
}
}
}
printf("%d\n", dp[vol][kth]);
}