前言
首先,大概讲一下什么是“背包”问题:背包问题是指你有一个容量为V的背包,然后有n个物品在你面前,你要怎么装才能使得背包里的物品总价值最大。而每种物品是只有1个,还是有多个,亦或是有无限个,这就是“01背包”、“多重背包”、“完全背包”的主要区别。
这里先打断一下,给自己一点时间,先思考一下这样的区别可能会在解法上有什么不同的区别,接着我们就开始往下看每种背包是怎么解决的。
01背包
假设你是一个小偷,在一个夜黑风高的晚上,偷偷摸摸地进了一个富人的别墅里,看到了有下面三件物品,你心想:美汁汁,这次赚大了。
正准备把这些物品全部打包带走,但是很不巧,这时候外面传来了开门的声音,你来不及打包了,只能将这些物品装进袋子赶紧跑路,这是一个空间大小为4磅的背包,问要怎么选才能使这次的收获最大。
先打断一下,肯定有人会疑惑为什么“01背包”要叫“01背包”,而不是“23背包”或者“45”背包???
很简单,顾名思义,每件物品只能选(1)或者不选(0),选就只能选一个,所以为1,不选就直接为0,所以叫做“01”。
看到这个问题,可能有人会说:那还不简单,直接把笔记本电脑和吉他装进去不就好了。的确如此,但是如果物品一多的时候,你就很难再这么轻松地判断出来了,这时候“动态规划”便应运而生。
接下来,我们开始做选择(也就是暴力递归的思想:尝试所有的选择)。
从上图可以看出,对于每个物品,我们都有两种选择:选/不选,这就产生了一共5种选择,最佳的是0,3500这个选择,接下来我们使用函数来表达一般化的情况。
暴力递归解法
我们先将具体问题转为一般情况的问题,假设有n件物品(x1,x2,...,xn),背包大小为C,每件物品的质量为Wi,价值为Vi,xi取0或1,表示第i个物品取或不取。接着按照暴力递归的三要素来写函数
函数的定义:f(i, j)表示背包剩余容量为i的时候,前j个物品最佳组合的价值
递推关系式(即当前调用单元做了什么):对于第j件物品,我们先判断背包剩余容量是否大于当前物品的质量
2.1 如果装不进,就跳过 f(i, j) = f(i,j-1)
2.2 装的进,有选或不选两种情况,取总价值大的,对应着f(i, j) = max(f(i,j-1), Vj + f(i - Wj, j - 1))
递归结束条件:背包剩余容量为0,或者已经遍历完了所有物品
理清楚了上面的三个条件,就很容易写出递归解法了。
public class Solution {
int[] vs = {3000, 2000, 1500}; //物品的价值
int[] ws = {4, 3, 1}; //物品的质量
public int maximumValue() {
int result = maximumValueHelper(4, 2);
return result;
}
private int maximumValueHelper(int i, int j) {
//base case:
if (j < 0 || i == 0) {
return 0;
}
int result = 0;
//判断当前容量能否装进第j件物品
if (i < ws[j]) {
result = maximumValueHelper(i, j - 1);
} else {
//不取第j件物品
int get = maximumValueHelper(i, j - 1);
//取第j件物品
int notGet = vs[j] + maximumValueHelper(i - ws[j], j - 1);
result = Math.max(get, notGet);
}
return result;
}
}
复制代码
带记忆数组的递归
在递归过程中,存在着大量的重复计算,所以可以使用一个“记忆数组”来减少重复计算。
public class Solution{
int[] vs = {3000, 2000, 1500}; //物品的价值
int[] ws = {4, 3, 1}; //物品的质量
//记忆数组:memo[i][j]表示背包剩余容量为i,第0~第j件物品的最佳组合的价值
int[][] memo = new int[5][3];
public int maximumValue() {
int result = maximumValueHelper(4, 2);
Arrays.fill(memo, -1);
return result;
}
private int maximumValueHelper(int i, int j) {
//base case:
if (j < 0 || i == 0) {
return 0;
}
//如果出现过,就直接返回
if (memo[i][j] != -1) {
return memo[i][j];
}
int result = 0;
//判断当前容量能否装进第j件物品
if (i < ws[j]) {
result = maximumValueHelper(i, j - 1);
} else {
//不取第j件物品
int get = maximumValueHelper(i, j - 1);
//取第j件物品
int notGet = vs[j] + maximumValueHelper(i - ws[j], j - 1);
result = Math.max(get, notGet);
}
memo[i][j] = result;
return result;
}
}
复制代码
动态规划
动态规划也是使用一个二维数组来减少重复计算,思路和“带记忆数组的递归”方法类似,不同的 是“动态规划”是自底向上,“带记忆数组的递归”是自顶向下。准备好一个二位数组之后,接下来就是简单的填表过程。
首先第一列dp[0][j],背包空间为0,所以最大价值都是0
接下来其他的格子按照递推式来填
public class Solution {
int[] vs = {3000, 2000, 1500}; //物品的价值
int[] ws = {4, 3, 1}; //物品的质量
public int maximumValue() {
int[][] dp = new int[5][3];
//填充第一行
for (int j = 0; j < dp[0].length; j++) {
dp[0][j] = 0;
}
//接下来的格子按照递推式来填
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
//如果背包剩余容量小于第j件物品的质量
if (i < ws[j]) {
dp[i][j] = dp[i][j - 1];
} else {
dp[i][j] = Math.max(dp[i][j - 1], vs[j] + dp[i - ws[j]][j - 1]);
}
}
}
return dp[4][2];
}
}
复制代码
多重背包
“多重背包”和“01背包”的区别就在于“多重背包”种每个物品的数量有多个,所以在选第j件物品的时候,可以选0个,或者1个,或者在背包容量足够的情况下全选。对照着“01背包”的递推式,我们可以写出“多重背包”的递推式f(i, j) = max(k * Vj + f(i - k * Wj, j - 1)) {k * wj <= i && k <= 第j件物品个数}
public class Solution {
int[] vs = {3000, 2000, 1500}; //物品的价值
int[] ws = {3, 2, 1}; //物品的质量
int[] nums = {3, 2, 4}; //对应每件物品的数量
public int maximumValue() {
//背包大小为10
int result = maximumValueHelper(10, 2);
return result;
}
private int maximumValueHelper(int i, int j) {
//base case:
if (j < 0 || i == 0) {
return 0;
}
int result = 0;
//判断当前容量能否装进第j件物品
if (i < ws[j]) {
result = maximumValueHelper(i, j - 1);
} else {
//第j件物品可以取0~nums[j]个
for (int k = 0; k <= nums[j] && k * ws[j] <= 10; k++) {
int tmp = k * vs[j] + maximumValueHelper(i - k * ws[j], j - 1);
result = tmp > result ? tmp : result;
}
}
return result;
}
}
复制代码
带记忆数组的递归仿照“01背包”中的解答可以写出,并没有实质上的改变。
动态规划
public class Solution{
int[] vs = {3000, 2000, 1500}; //物品的价值
int[] ws = {3, 2, 1}; //物品的质量
int[] nums = {3, 2, 4}; //对应物品的个数
public int maximumValue() {
//背包大小为10
int[][] dp = new int[11][3];
//填充第一行
for (int j = 0; j < dp[0].length; j++) {
dp[0][j] = 0;
}
//接下来的格子按照递推式来填
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
//如果背包剩余容量小于第j件物品的质量
if (i < ws[j]) {
dp[i][j] = dp[i][j - 1];
} else {
//第j件物品可以取多个
for (int k = 0; k < nums[j] && k * ws[j] <= i; k++) {
dp[i][j] = Math.max(dp[i][j], k * vs[j] + dp[i - k * ws[j]][j - 1]);
}
}
}
}
return dp[10][2];
}
}
复制代码
完全背包
完全背包的特点是每件物品可以取无限次,所以相比于多重背包少了一个约束条件k
public class Solution{
int[] vs = {3000, 2000, 1500}; //物品的价值
int[] ws = {3, 2, 1}; //物品的质量
public int maximumValue() {
//背包大小为10
int[][] dp = new int[11][3];
//填充第一行
for (int j = 0; j < dp[0].length; j++) {
dp[0][j] = 0;
}
//接下来的格子按照递推式来填
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
//如果背包剩余容量小于第j件物品的质量
if (i < ws[j]) {
dp[i][j] = dp[i][j - 1];
} else {
//第j件物品可以取多个
for (int k = 0; k * ws[j] <= i; k++) {
dp[i][j] = Math.max(dp[i][j], k * vs[j] + dp[i - k * ws[j]][j - 1]);
}
}
}
}
return dp[10][2];
}
}
复制代码
总结
“完全背包”和“多重背包”是“01背包”的扩展,其本质区别是物品可以取多少个,只要理清楚了这个区别,这三种背包问题也就不是事了。
关于找一找教程网
本站文章仅代表作者观点,不代表本站立场,所有文章非营利性免费分享。
本站提供了软件编程、网站开发技术、服务器运维、人工智能等等IT技术文章,希望广大程序员努力学习,让我们用科技改变世界。
[一次性解决三种背包问题]http://www.zyiz.net/tech/detail-118277.html