- Backpack VII
Assume that you have n yuan. There are many kinds of rice in the supermarket. Each kind of rice is bagged and must be purchased in the whole bag. Given the weight, price and quantity of each type of rice, find the maximum weight of rice that you can purchase.
Example
Example 1:
Input: n = 8, prices = [3,2], weights = [300,160], amounts = [1,6]
Output: 640
Explanation: Buy the second rice(price = 2) use all 8 money.
Example 2:
Input: n = 8, prices = [2,4], weight = [100,100], amounts = [4,2 ]
Output: 400
Explanation: Buy the first rice(price = 2) use all 8 money.
多重背包问题可以视为01背包的变形。
解法1:没有经过优化的DP。
dp[i][j] 表示前i件物品在价值不超过j的情况下的最大重量。
若输入为:
8 //n = money
[3,2] //prices
[300,160] //weight
[1,6] //amounts
输出为 640
dp[][] 为:
0 0 0 0 0 0 0 0 0
0 0 0 300 300 300 300 300 300
0 0 160 300 320 460 480 620 640
注意这里不能用贪婪法。比如说先花3元买weight为300的物品,然后剩下5元买两件weight为160的物品。这样重量为620,非最优。原因是这样会剩下1元用不上!
时间复杂度O(NW): N是所有item的个数的总和(注意每个item可能有若干个), W是总的money数。
空间复杂度O(nW): n是item种数,W是总的money数。
class Solution {
public:
/**
* @param n: the money of you
* @param prices: the price of rice[i]
* @param weight: the weight of rice[i]
* @param amounts: the amount of rice[i]
* @return: the maximum weight
*/
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int itemCount = prices.size(); //count of items
vector<vector<int>> dp(itemCount + 1, vector<int>(n + 1, 0)); //dp[i][j] means the maximum weight of first i tems with total price <= j
for (int i = 1; i <= itemCount; ++i) { // 遍历每种物品
for (int k = 1; k <=n; ++k) {
dp[i][k] = dp[i - 1][k];
}
for (int j = 0; j <= amounts[i - 1]; ++j) { // 对每种物品,遍历其个数
for (int k = 1; k <= n; ++k) { //k-> 1 .. n
if (k >= j * prices[i - 1]) {
dp[i][k] = max(dp[i][k], dp[i - 1][k - j * prices[i - 1]] + j * weight[i - 1]);
}
}
}
}
return dp[itemCount][n];
}
};
上面的循环j和循环k可以i交换。
另外还要注意,上面的三层循环不能写成下面这样。原因是dp[i][k]被 j 循环和 i 循环改写了,而dp[i][k]应该是独立于 j 循环的。
for (int i = 1; i <= itemCount; ++i) { // i->item[i]
for (int j = 0; j <= amounts[i - 1]; ++j) { //j-> amount[0]..amount[i-1]
for (int k = 1; k <= n; k++) { //k-> 1 .. n
dp[i][k] = dp[i - 1][k]; //应该提出去
if (k >= j * prices[i - 1]) {
dp[i][k] = max(dp[i][k], dp[i - 1][k - j * prices[i - 1]] + j * weight[i - 1]);
}
}
}
}
写成下面这样是可以的,而且更清楚。
class Solution {
public:
/**
* @param n: the money of you
* @param prices: the price of rice[i]
* @param weight: the weight of rice[i]
* @param amounts: the amount of rice[i]
* @return: the maximum weight
*/
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int itemCount = prices.size(); //count of items
vector<vector<int>> dp(itemCount + 1, vector<int>(n + 1, 0)); //dp[i][j] means the maximum weight of first i tems with total price <= j
for (int i = 1; i <= itemCount; ++i) {
for (int k = 1; k <= n; ++k) { //k-> 1 .. n
dp[i][k] = dp[i - 1][k];
for (int j = 0; j <= amounts[i - 1]; ++j) {
if (k >= j * prices[i - 1]) {
dp[i][k] = max(dp[i][k], dp[i - 1][k - j * prices[i - 1]] + j * weight[i - 1]);
}
}
}
}
return dp[itemCount][n];
}
};
写成下面这样也可以
class Solution {
public:
/**
* @param n: the money of you
* @param prices: the price of rice[i]
* @param weight: the weight of rice[i]
* @param amounts: the amount of rice[i]
* @return: the maximum weight
*/
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int itemCount = prices.size(); //count of items
vector<vector<int>> dp(itemCount + 1, vector<int>(n + 1, 0)); //dp[i][j] means the maximum weight of first i tems with total price <= j
for (int i = 1; i <= itemCount; ++i) {
for (int k = 1; k <= n; ++k) { //k-> 1 .. n
for (int j = 0; j <= amounts[i - 1]; ++j) { //循环j,k可互换,j必须从0开始
if (k >= j * prices[i - 1]) {
dp[i][k] = max(dp[i][k], dp[i - 1][k - j * prices[i - 1]] + j * weight[i - 1]);
} else {
dp[i][k] = max(dp[i - 1][k], dp[i][k]);
}
}
}
}
return dp[itemCount][n];
}
};
解法2: 在解法1的基础上加上一维数组优化。
时间复杂度还是O(NW)。
空间复杂度O(W)。
dp[i]: the maximum weight of rice that price i can get.
class Solution {
public:
/**
* @param n: the money of you
* @param prices: the price of rice[i]
* @param weight: the weight of rice[i]
* @param amounts: the amount of rice[i]
* @return: the maximum weight
*/
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int itemCount = prices.size(); //count of items
vector<int> dp(n + 1, 0);
for (int i = 1; i <= itemCount; ++i) {
//cout<<"i="<<i<<endl;
for (int j = 1; j <= amounts[i - 1]; ++j) { //注意,这里j必须从1开始
for (int k = n; k >= prices[i - 1]; --k) {
dp[k] = max(dp[k], dp[k - prices[i - 1]] + weight[i - 1]);
//cout<<"dp["<<k<<"]="<<dp[k]<<endl;
}
}
}
return dp[n];
}
};
如何理解上面的代码呢?
以input =
8 //n=8
[3,2] //prices
[300,160] //weights
[1,6] //amounts
为例
i 小=>大
j 小=>大
k 大=>小
i=1 //处理到第1件物品
dp[8]=300 dp[7]=300 dp[6]=300 dp[5]=300 dp[4]=300 dp[3]=300 //j = 1
从dp[8]开始都是300。只买1个第1件物品,因为第1件物品只有1个。dp[8] = dp[5]+300。而此时dp[5]=0。这就是为什么k要从大到小来。
没有dp[2]和dp[1]因为第1件物品价格是300。dp[2]=0, dp[1]=0。
i=2 //处理到第2件物品
dp[8]=460 dp[7]=460 dp[6]=460 dp[5]=460 dp[4]=300 dp[3]=300 dp[2]=160 //j = 1
dp[8]=460,因为可以用8块钱买1个第1件物品和1个第2件物品,dp[8] = dp[6] + 160 = 460。此时dp[6] = 300。多3块钱没用。注意只能买1个第2件物品,因为j=1。
dp[7]=460,因为可以用7块钱买1个第1件物品和1个第2件物品,dp[7] = dp[5] + 160 = 460。此时dp[5] = 300。多2块钱没用。注意只能买1个第2件物品,因为j=1。
dp[6]=460,因为可以用6块钱买1个第1件物品和1个第2件物品,dp[6] = dp[4] + 160 = 460。此时dp[4] = 300。多1块钱没用。注意只能买1个第2件物品,因为j=1。
dp[5]=460,因为可以用5块钱买1个第1件物品和1个第2件物品。dp[5] = dp[3] + 160 = 460。此时dp[3] = 300。
dp[4]=300,因为可以用3块钱买1个第1件物品,dp[4] = 300 > dp[2] + 160 (160),所以dp[4]不变。多1块钱没用。买不了东西。
dp[3]=300,因为可以用3块钱买1个第1件物品。dp[3] = 300 > dp[1] + 160 (160),所以dp[3]不变。
dp[2]=160,因为可以用2块钱买1个第2件物品。dp[2] = 160 > 0.
…
dp[8]=620 dp[7]=620 dp[6]=460 dp[5]=460 dp[4]=320 dp[3]=300 dp[2]=160 //j = 2
dp[8]变了,因为dp[8] = dp[6] + 160 = 620 > old dp[8] (460)
dp[7]变了,因为dp[7] = dp[5] + 160 = 620 > old dp[7] (460)
dp[4]变了,因为dp[4] = dp[2] + 160 = 320 > old dp[4] (300)
dp[8]=620 dp[7]=620 dp[6]=480 dp[5]=460 dp[4]=320 dp[3]=300 dp[2]=160 //j = 3
dp[6]变了,因为dp[6] = dp[4] + 160 = 320 + 160 = 480 > 460
dp[8]=640 dp[7]=620 dp[6]=480 dp[5]=460 dp[4]=320 dp[3]=300 dp[2]=160 //j = 4
dp[8]变了,因为dp[8] = dp[6] + 160 = 480 + 160 = 640 > 620
dp[8]=640 dp[7]=620 dp[6]=480 dp[5]=460 dp[4]=320 dp[3]=300 dp[2]=160 //j = 5
dp[8]=640 dp[7]=620 dp[6]=480 dp[5]=460 dp[4]=320 dp[3]=300 dp[2]=160 //j = 6
Output
640
注意,上面的j循环必须从1开始,因为
dp[k] = max(dp[k], dp[k - prices[i - 1]] + weight[i - 1])
里面的dp[k - prices[i - 1]]都会减去一个prices[i - 1],即取当前item的price。如果从0开始,就会导致取了amount[i - 1] + 1次。
大家可能有个疑问就是某item难道不能不取吗?当然可以不取,这样的话,手头的钱就买别的item了,如果能买得起的话。
我们也可以换个思维方式
dp[k] = max(dp[k], dp[k - prices[i - 1]] + weight[i - 1])
这里等号右边的dp[k]就相当于当前item不买,dp[k - prices[i - 1]] + weight[i - 1] 就相当于当前item至少买一个。
这样就好理解了。
上面的代码也可以写成如下形式,这样数组下标从i-1变成了i。
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int itemCount = prices.size(); //count of items
vector<int> dp(n + 1, 0);
for (int i = 0; i < itemCount; ++i) {
//cout<<"i="<<i<<endl;
for (int j = 1; j < amounts[i]; ++j) {
for (int k = n; k >= prices[i]; --k) {
dp[k] = max(dp[k], dp[k - prices[i]] + weight[i]);
//cout<<"dp["<<k<<"]="<<dp[k]<<endl;
}
}
}
return dp[n];
}
注意:
1). 这里不能写成
for (int i = 1; i <= itemCount; ++i) {
for (int j = 1; j <= amounts[i - 1]; ++j) {
for (int k = n; k >= j * prices[i - 1]; --k) {
dp[k] = max(dp[k], dp[k - j*prices[i - 1]] + j*weight[i - 1]);
}
}
}
为什么在解法1中我们要加 j*, 而解法2就不能加j*呢?
先回顾一下解法1和解法2的区别:
解法1是二维数组。k是从小到大。第i次循环时,对于某个具体的dp[i][k]值,应该从
dp[i-1][k-j*prices[i-1]]+j*weight[i - 1]), j=0…amounts[i-1]中找。
dp[i-1][]存储的是第i-1次循环的结果。
解法2是一维数组,k从大到小,若k2<k1,dp[k1]可以参考dp[k2]的值,因为此时dp[k2]还未更新,还是上次i-1循环时的值。所以解法2和解法1是等价的。
下面分析加j*的原因。因为DP算法中,我们每次要为dp[i][k]或dp[k]找一个尽可能大的(这里是求最大值)candidate与dp[i][k]或d[k]比较。
解法1里面,k从小到大,所以dp[i][k]能找到的最大candidate就是dp[i - 1][k - j * prices[i - 1]] + j * weight[i - 1]) 里面的某一个j所代表的值。
假设我们把上面的转移方程写成
dp[k] = max(dp[k], dp[k - j * prices[i - 1]] + j * weight[i - 1]);
会怎么样呢? 我们还是以上面的input为例来看
以input =
8 //n=8
[3,2] //prices
[300,160] //weights
[1,6] //amounts
为例,其打印输出如下
i=1
dp[8]=300 dp[7]=300 dp[6]=300 dp[5]=300 dp[4]=300 dp[3]=300
i=2
dp[8]=460 dp[7]=460 dp[6]=460 dp[5]=460 dp[4]=300 dp[3]=300 dp[2]=160 //j=1
跟上面没有 j * 的情况一致。
dp[8]=620 dp[7]=620 dp[6]=480 dp[5]=460 dp[4]=320 dp[3]=320 dp[2]=369 //j=2
我们看出,此时dp[6]=480,而上面没有 j * 的情况dp[6]=460。这里的480是怎么来的呢?
因为dp[6] = dp[6 - 2 * 2] + 2 * 160 = dp[2] + 2 * 160 = 480。这就不对了,因为j=2,但是已经买了3个第2件物品。所以我们可以看出,这里的问题是没有办法控制某个物品买的个数不超过该物品的实际件数。而如果不加j*的话,每次都只是跟上面j-1次的结果比较,正好可以控制。
dp[8]=849 dp[7]=620 dp[6]=480 dp[5]=480 dp[4]=529 dp[3]=480 dp[2]=480 //j=3
dp[8]=849 dp[7]=640 dp[6]=689 dp[5]=640 dp[4]=640 dp[3]=640 dp[2]=640 //j=4
dp[8]=849 dp[7]=800 dp[6]=800 dp[5]=800 dp[4]=800 dp[3]=800 dp[2]=800 //j=5
dp[8]=960 dp[7]=960 dp[6]=960 dp[5]=960 dp[4]=960 dp[3]=960 dp[2]=960 //j=6
Output = 960 但正确答案是640。
2). 注意这里 j 循环必须是从1开始。因为j 循环实际上就是一个计数而已,如果从0开始就多计算了一次。这样的话dp[k - (amounts[i-1]+1)*prices[i - 1]]并没有意义,因为并没有那么多该物品可以取,它可能是不取别的物品的最优值。
而解法1中j 循环可以从0开始,也可以从1开始。从0开始无非就是多做了一次
dp[i][k] = max(dp[i][k], dp[i - 1][k]);
而已,因为0跟任何数相乘仍然为0。
解法3:在解法1的基础上加上滚动数组优化。
class Solution {
public:
/**
* @param n: the money of you
* @param prices: the price of rice[i]
* @param weight: the weight of rice[i]
* @param amounts: the amount of rice[i]
* @return: the maximum weight
*/
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int itemCount = prices.size(); //count of items
vector<vector<int>> dp(2, vector<int>(n + 1, 0)); //dp[i][j] means the maximum weight of first i tems with total price <= j
for (int i = 1; i <= itemCount; ++i) { // 遍历每种物品
for (int k = 1; k <=n; ++k) {
dp[i % 2][k] = dp[(i - 1) % 2][k];
}
for (int j = 0; j <= amounts[i - 1]; ++j) { // 对每种物品,遍历其个数
for (int k = 1; k <= n; ++k) { //k-> 1 .. n
if (k >= j * prices[i - 1]) {
dp[i % 2][k] = max(dp[i % 2][k], dp[(i - 1) % 2][k - j * prices[i - 1]] + j * weight[i - 1]);
}
}
}
}
return dp[itemCount % 2][n];
}
};
解法4:二进制优化
class Solution {
public:
/**
* @param n: the money of you
* @param prices: the price of rice[i]
* @param weight: the weight of rice[i]
* @param amounts: the amount of rice[i]
* @return: the maximum weight
*/
int backPackVII(int n, vector<int> &prices, vector<int> &weight, vector<int> &amounts) {
int m = prices.size();
vector<int> dp(n + 1, 0);
for (int i = 0; i < m; ++i) {
int s = amounts[i];
for (int j = 1; s; j *= 2) {
if (j > s) j = s; //j代表这堆多少个
s -= j;
for (int k = n; k >= j * prices[i]; k--) {
dp[k] = max(dp[k], dp[k - j * prices[i]] + j * weight[i]);
}
}
}
return dp[n];
}
};
与解法2的对比,主要是循环j每次步进长度为1,2,4,8,…最后一轮不够2^n次方时就全丢给最后一轮。
为什么1,2,4,8这样的2进制步长可以呢?因为它们的不同组合就可以组成1…amounts[i]中的任何一个数。
这样,时间复杂度就降低为 O(n×m×∑i=1i=nlogsi)O(n \times m \times \sum_{i=1}^{i=n}{logs_i})O(n×m×∑i=1i=nlogsi)
注意:
- 上面是用的一维数组,但是转移方程要用j*, 跟方法2区分!
解法5:单调队列优化 //终极优化,时间复杂度降低为O(nm)。
TBD
本文深入解析多重背包问题,一种经典的动态规划题目。通过实例详细解释了四种不同的解决方案,包括未优化的DP、一维数组优化、滚动数组优化及二进制优化,最后提及了使用单调队列的终极优化方法。
319

被折叠的 条评论
为什么被折叠?



