今天就来揭开多重背包的面纱!!!
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了今天就来揭开多重背包的面纱!!!相关的知识,希望对你有一定的参考价值。
题目描述
有 N 种物品和一个容量为 C 的背包,每种物品「数量有限」。
第 i 件物品的体积是 v[i],价值是 w[i],数量为 s[i] 。
问选择哪些物品,每件物品选择多少件,可使得总价值最大。
其实就是在 0-1 背包问题的基础上,增加了每件物品可以选择「有限次数」的特点(在容量允许的情况下)。
示例 1:
输入: N = 2, C = 5, v = [1,2], w = [1,2], s = [2,1]
输出: 4
解释: 选两件物品 1,再选一件物品 2,可使价值最大。
朴素二维
首先,几乎所有的「背包问题」都是基于「01 背包」演变而来。
因此,当我们确定一个问题是背包问题之后,可以根据其物品的「数量限制」来判别是何种背包问题,然后套用「01 背包」的思路来求解。
具体的,我们可以套用「01 背包」的「状态定义」来进行分析:
dp[i][j] 代表考虑前 i 件物品,且所选物品总体积不超过 j 时获得的最大价值。
由于每件物品可以被选择「有限次」,因此对于某个 dp[i][j] 而言,其值应该为以下所有可能方案中的最大值:
- 选择0件物品i的最大价值,即dp[i-1][j]
- 选择1件物品i的最大价值,即dp[i-1][j-v[i]]+w[i]
- 选择2件物品i的最大价值,即dp[i-1][j-2 * v[i]]+2 * w[i]
- …
- 选择s[i] 件物品i的最大价值,即dp[i-1][j-s[i] * v[i]]+s[i] * w[i]
由此我们可以得出「状态转移方程」为:
可以发现其状态转移方程与 完全背包 完全一致,只是多了 0<k<=s[i] 的条件。
也好理解,毕竟「完全背包」不限制物品数量,「多重背包」限制物品数量。
c++完整测试代码
#include<iostream>
#include<vector>
using namespace std;
int s[100], v[100], w[100];//第i件物品最多有Si件,每件体积是Vi,价值是Wi
class Solution
{
public:
int maxValue(int N, int V, int s[], int v[], int w[])
{
vector<vector<int>> dp(N, vector<int>(V + 1, 0));
// 先处理第一件物品
for (int i = 0; i <= V; i++)
{
// 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件(不超过限制数量)
int maxK = min(i / v[0], s[0]);
dp[0][i] = maxK * w[0];
}
// 处理剩余物品
for (int i = 1; i < N; i++)
{
for (int j = 0; j <=V; j++)
{
// 不考虑第 i 件物品的情况
dp[i][j] = dp[i - 1][j];
// 考虑第 i 件物品的情况
for (int k = 1; k <= s[i]; k++)
{
//判断当前背包能否放下
if (j < k * v[i]) break;
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k*w[i]);
}
}
}
return dp[N - 1][V];
}
};
int main()
{
int N, V;//N种物品,背包容量为V
cin >> N >> V;
for (int i = 0; i < N; i++)
cin >> v[i] >> w[i] >> s[i];
Solution sol;
cout << sol.maxValue(N,V,s,v,w) << endl;
return 0;
}
滚动数组
通过观察我们的「状态转移方程」可以发现,我们在更新某个 dp[i][x] 的时候只依赖于dp[i-1][y] 。
因此我们可以像 01 背包那样使用「滚动数组」的方式将空间优化到 O©。
代码:
#include<iostream>
#include<vector>
using namespace std;
int s[100], v[100], w[100];//第i件物品最多有Si件,每件体积是Vi,价值是Wi
class Solution
{
public:
int maxValue(int N, int V, int s[], int v[], int w[])
{
vector<vector<int>> dp(2, vector<int>(V + 1, 0));
// 先处理第一件物品
for (int i = 0; i <= V; i++)
{
// 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件(不超过限制数量)
int maxK = min(i / v[0], s[0]);
dp[0][i] = maxK * w[0];
}
// 处理剩余物品
for (int i = 1; i < N; i++)
{
for (int j = 0; j <=V; j++)
{
// 不考虑第 i 件物品的情况
dp[i&1][j] = dp[(i - 1)&1][j];
// 考虑第 i 件物品的情况
for (int k = 1; k <= s[i]; k++)
{
//判断当前背包能否放下
if (j < k * v[i]) break;
dp[i&1][j] = max(dp[i&1][j], dp[(i - 1)&1][j - k * v[i]] + k*w[i]);
}
}
}
return dp[(N - 1)&1][V];
}
};
int main()
{
int N, V;//N种物品,背包容量为V
cin >> N >> V;
for (int i = 0; i < N; i++)
cin >> v[i] >> w[i] >> s[i];
Solution sol;
cout << sol.maxValue(N,V,s,v,w) << endl;
return 0;
}
一维空间优化
在之前的「01 背包」和「完全背包」都可以进行「一维空间优化」。
其中「完全背包」的「一维空间优化」还能有效降低时间复杂度。
那么「多重背包」是否也能通过「一维空间优化」来降低时间复杂度呢?
答案是可以优化空间,但是不能降低时间复杂度。
因为当我们像「完全背包」那样只保留「容量维度」,并且「从小到大」遍历容量的话,我们在转移 时是无法直接知道所依赖的 到底使用了多少件物品 的。
这个问题在「完全背包」里面无须关心,因为每件物品可以被选择无限次,而在「多重背包」则是不能忽略,否则可能会违背物品件数有限的条件。
因此,「多重背包」问题的「一维空间优化」并不能像「完全背包」那样使复杂度降低。
代码:
#include<iostream>
#include<vector>
using namespace std;
int s[100], v[100], w[100];//第i件物品最多有Si件,每件体积是Vi,价值是Wi
class Solution
{
public:
int maxValue(int N, int V, int s[], int v[], int w[])
{
vector<int> dp(V + 1);
for (int i = 0; i < N; i++)//物品
{
for (int j = V; j >= v[i];j--)//容量---从大到小---因为无法像完全背包那样每个物品都无限次放入
{
for (int k = 0; k <= s[i] && k * v[i] <= j; k++)//物品数量
{
dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
}
return dp[V];
}
};
int main()
{
int N, V;//N种物品,背包容量为V
cin >> N >> V;
for (int i = 0; i < N; i++)
cin >> v[i] >> w[i] >> s[i];
Solution sol;
cout << sol.maxValue(N,V,s,v,w) << endl;
return 0;
}
容量遍历---从大到小---因为无法像完全背包那样每个物品都无限次放入,因此要增加一重循环,防止数据覆盖,从尾到头覆盖旧数据
与其他背包的内在关系
至此,三类传统背包问题的「一维空间优化」方式都已经讲过了。
我们发现,只有「完全背包」的「一维空间优化」是存在数学意义上的优化(能够有效降低算法时间复杂度)。
「01 背包」和「多重背包」的「一维空间优化」其实只是基于「朴素二维」解法做单纯的「滚动」操作而已(利用状态之间的依赖关系,配合遍历顺序,使得不再需要参与转移的空间能够被重新利用)。
因此,一定程度上,可以将「多重背包」看做是一种特殊的「01 背包」。
对「01 背包」中具有相同的价值 & 成本的物品进行计数,就成了对应物品的「限制件数」,「01 背包」也就转换成了「多重背包」。
同理,将「多重背包」的多件物品进行「扁平化展开」,就转换成了「01 背包」。
总结
无论是「朴素二维」、「滚动数组」、「一维优化」还是「扁平化」都不能优化「多重背包」问题的时间复杂度。
同时,我们能总结出:在传统的三种背包问题的「一维空间优化」里,只有「完全背包」的「容量维度」是「从小到大」的,其他两种背包的「容量维度」都是「从大到小」的。
以上是关于今天就来揭开多重背包的面纱!!!的主要内容,如果未能解决你的问题,请参考以下文章