题目描述
难度分:2018
输入n(1≤n≤3000)、m(1≤m≤3000)表示有n种物品,以及一个容量为m的背包。
然后输入n行,每行输入第i种物品的体积w(1≤w≤m)和价值v(1≤v≤109)。
每种物品都有无限个。为了避免选择过多相同类型的物品,有如下规定:
- 如果同一种物品选了k个,那么这k个物品的实际总价值不是k×v,而是k×v−k2。
输出所选物品的价值总和的最大值。物品的体积之和不能超过m。
输入样例1
2 10
3 4
3 2
输出样例1
5
输入样例2
3 6
1 4
2 3
2 7
输出样例2
14
输入样例3
1 10
1 7
输出样例3
12
算法
动态规划
本质上还是个完全背包问题,因此循环的顺序还是一样的。我们将相同体积的价值放入映射vec,它表示“体积→价值数组”。
状态定义
dp[j]表示所选物品的体积为j的情况下能够获得的最大价值,在这个定义下,答案就是maxj∈[0,W]dp[j]。
状态转移
初始情况下dp[0]=0。对于一般情况,分别考虑每种体积,遍历w∈[1,W],看体积为w的物品选多少个k。根据完全背包套路,外层循环倒序遍历j∈[0,W],内层循环正序遍历k,需要满足k×w≤W不能超过背包容量。状态值转移方程为dp[j]=max(dp[j],d[j−k×w]+happiness[k]),happiness[k]表示选k个体积为w的物品可以得到的最大价值。
考虑怎么预处理出happiness数组,对于价值v,如果选一个这样的物品,价值为v−1。当选择的数量增加,增量即为k×v−k2−((k−1)×v−(k−1)2)=v−2×k+1。所以k=1时自增为v−1,k=2时增量为v−3,k=3时增量为v−5,……,因此每选一个体积为w的物品,下次再选它,价值增量就会变为v−2。初始化happiness[0]=0,用一个大根堆heap来预处理出happiness数组,在k×v≤W的限制下遍历k,happiness[k]=happiness[k−1]+v,(其中v为大根堆的堆顶,初始情况下存的是vec[w]中所有价值的增量v−1,也就是k=1时的增量),弹出堆顶之后再压入v−2到heap中。
复杂度分析
时间复杂度
外层循环遍历w∈[1,W]的时间复杂度为O(W),内部要先处理happiness,根据调和级数,这部分的时间复杂度为O(WlnW)。预处理之后还要进行完全背包,需要遍历j∈[0,W],对于每个j,又要遍历满足k×w≤W的k,根据调和级数这部分的时间复杂度为O(W2lnW)。因此,整个算法的时间复杂度为O(W2lnW)。
空间复杂度
vec中存了所有O(n)个物品的w和v,空间复杂度为O(n)。而动态规划时所需要的DP
数组dp以及happiness数组的空间复杂度都是O(W)(w=1,k可以是W)。因此,整个算法的额外空间复杂度为O(n+W)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3001;
int n, W;
vector<int> vec[N];
LL dp[N], happiness[N];
int main() {
scanf("%d%d", &n, &W);
for(int i = 1; i <= n; i++) {
int w, v;
scanf("%d%d", &w, &v);
vec[w].push_back(v - 1);
}
for(int i = 0; i <= W; i++) {
dp[i] = -1e18;
}
dp[0] = 0;
LL ans = 0;
for(int w = 1; w <= W; w++) {
if(vec[w].empty()) continue;
priority_queue<int> heap;
for(int v: vec[w]) {
heap.push(v);
}
happiness[0] = 0;
for(LL k = 1; k*w <= W; k++) {
int v = heap.top();
heap.pop();
happiness[k] = happiness[k - 1] + v;
heap.push(v - 2);
}
for(int j = W; j >= 0; j--) {
for(LL k = 0; k * w <= j; k++) {
dp[j] = max(dp[j], dp[j - k*w] + happiness[k]);
}
ans = max(ans, dp[j]);
}
}
printf("%lld\n", ans);
return 0;
}