例三:多重背包
多重背包问题我们经常能碰到,就是给出n种物品,每种物品有a[i]
个,占用空间为v[i]
,价值为w[i]
,背包容量为K,问背包所能装下的物品的最大价值
方法一:
我们一般的求法就是开一个dp[k]
数组代表空间容量为i时的最大价值。然后遍历每种物品的每件,通过递推公式:
dp[k] = max(dp[k], dp[k - v[i]] + w[i]);
这时时间复杂度为O(n * m * K)
(其中n为种类数,m为每种物品的数量,K为背包容量)
但是,当m特别大时,我们应该怎么办呢?
方法二:
我们会发现,每种的物品,它们的v[i]和w[i]
是相同的。假如某种物品有15种,我们上面那种方法的做法,由于是遍历该种的每一件物品,因此也就是将{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}任意组合。我们会发现组合会有很多重复,因此我们可以这样进行优化:
对于集合{1, 2, 4, 8},我们也可以通过这四个数组合成1~15个的任意一个。这样我们只需要求每种物品的n件物品优化成logn种物品,这样时间复杂度就可以优化为O(n * log(m) * K)
注意点!!!!如果一种物品有n件,所选的集合必须只能组成[1, n]
,比如13:{1, 2, 4, 6}
这种方法的代码如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
int dp[10005] = {0};
int main () {
int n, K, v, w, num;
scanf("%d%d", &K, &n); //K代表背包容量,n代表物品种类数
for (int i = 0; i < n; i++) {
scanf("%d%d%d", &v, &w, &num); //v,w,num分别代表每种物品的体积,价值,数量
int val = 1; //val为集合所有元素的和,初始化为1
int j = 1; //j为集合为集合中的元素,初始化为1
while (val <= num) {
int vv = v * j; //此时把j个该种物品看作一个物品进行dp,也就是01背包
int ww = w * j;
for (int p = K; p >= vv; p--) {
dp[p] = max(dp[p], dp[p - vv] + ww);
}
j = val + 1;
val += j;
}
val -= j; //该操作保证所求集合确保正好组成[1, num]
j = num - val;
if (j > 0) {
int vv = v * j;
int ww = w * j;
for (int p = K; p >= vv; p--) {
dp[p] = max(dp[p], dp[p - vv] + ww);
}
}
}
printf("%d\n", dp[K]);
return 0;
}
方法二时间复杂度为O(n * logm * K)
,还不可以继续优化呢?答案是可以的,我们可以利用单调队列对多重背包问题进行优化到O(n * K)
。大家可以先自己考虑一下如何去做再往下看
方法三:
方法三看了好多好的题解才搞懂。。只能说自己太笨了Orz~以下是我的理解,希望能对大家有些帮助
我们单拿出一种物品来看,比如体积为5,价值为6,数量为3,再假如K为22。那么我们单拿出全部%5 == 2的容量来看:
dp[2] = dp[2];
dp[7] = max(dp[2], dp[7] - 6) + 6;
dp[12] = max(dp[2], dp[7] - 6, dp[12] - 12) + 12;
dp[17] = max(dp[2], dp[7] - 6, dp[12] - 12, dp[17] - 18) + 18;
dp[22] = max(dp[7] - 6, dp[12] - 12, dp[17] - 18, dp[22] - 24) + 24;
//这一步没看懂的同学看这里!!
比如dp[22] = max(dp[7] - 6, dp[12] - 12, dp[17] - 18, dp[22] - 24) + 24中,
dp[7] - 6若是最大值,dp[7] - 6 + 24等价于dp[22] = dp[7] + 18,也就是用3个该种物品
dp[12] - 12若是最大值,dp[12] - 12 + 24等价于dp[22] = dp[12] + 12,也就是用2个该种商品
dp[17] - 18若是最大值,dp[17] - 18 + 24等价于dp[22] = dp[17] + 6,也就是用1个该种商品
dp[22] - 24若是最大值,dp[22] - 24 + 24等价于dp[22] = dp[22],也就是不用该种商品
至于最多选用几个的数量s,它等于s = min(s, K / v)。这样,我们就可以将题目转化为下面的代码形式(伪代码):
for (循环遍历每种商品) {
for (循环遍历全部余数d[0, v)) {
for (循环遍历K以内全部余数为d的容量k*v+d) {
for (循环遍历s个上一种物品的状态并更新最大值) {
}
}
}
}
我们从伪代码可以看出时间复杂度为O(n * K * s)
(第二层和第三层for相乘为K),和上一种方法复杂度差不多甚至在某些情况下还要更高。我们可以用单调队列维护最大值且区间差值小于等于s来优化该算法,此时复杂度为O(n * K)
比如说:
将dp[2] - 0 * 6加入单调队列并维护最大值,然后用队列最大值 + 0 * 6即为新的dp[2]
将dp[7] - 1 * 6加入单调队列并维护最大值,然后用队列最大值 + 1 * 6即为新的dp[7]
将dp[12] - 2 * 6加入单调队列并维护最大值,然后用队列最大值 + 2 * 6即为新的dp[12]
将dp[17] - 3 * 6加入单调队列并维护最大值,然后用队列最大值 + 3 * 6即为新的dp[17]
将dp[22] - 4 * 6加入单调队列并维护最大值,然后用队列最大值 + 4 * 6即为新的dp[22]
//注意单调队列的维护需要注意的两点(1.若q[tail] <= val则循环tail--直到满足条件为止 2.若q[tail]与q[head]相差数量大于s则循环head++直到满足条件为止)
下面给出这道题单调队列优化的代码:
#include <stdio.h>
#include <algorithm>
using namespace std;
//dp[i]存储当前状态下容量为i的最大价值
//q[i]用于模拟优先队列
//inv[i]用于存储优先队列中位置为i的使用该种商品的物品数
int dp[10005] = {0}, inv[10005] = {0}, q[10005] = {0};
int main () {
int K, n, v, w, s;
scanf("%d%d", &K, &n);
for (int i = 0; i < n; i++) {
scanf("%d%d%d", &v, &w, &s);
s = min(K / v, s); //代表能用的该种物品的最大数量
for (int mod = 0; mod < v; mod++) {
int head = 1, tail = 1; //相当于清空队列
for (int j = 0; j <= (K - mod) / v; j++) {
int val = dp[j * v + mod] - j * w;
while (head < tail && q[tail - 1] <= val) {
tail--; //该操作保证队列单调
}
q[tail] = val;
inv[tail++] = j;
while (head < tail && j - inv[head] > s) {
head++; //该操作用于维护数量上限
}
//此时队列的头(即最大值)就是我们需要使用该种商品的数量
dp[j * v + mod] = max(q[head] + j * w, dp[j * v + mod]);
}
}
}
printf("%d\n", dp[K]);
return 0;
}
如果有写的不对或者不全面的地方 可通过主页的联系方式进行指正,谢谢