学算法的时候,背包问题是一个很常见的动态规划问题,像什么01背包、完全背包、多重背包等,当时学的时候就有一些懵懵懂懂的,现在复习的时候又不会了,所以进行总结一下,方便日后查看学习。问题基本上都是lintcode上面的题目,然后在github上还找到一个专门讲背包问题的仓库:https://github.com/tianyicui/pack
在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i],题目地址:https://www.lintcode.com/problem/backpack/description
样例 1:
输入: [3,4,8,5], backpack size=10
输出: 9
样例 2:
输入: [2,3,5,7], backpack size=12
输出: 12
O(n x m) 的时间复杂度 and O(m) 空间复杂度
如果不知道如何优化空间O(n x m) 的空间复杂度也可以通过.
你不可以将物品进行切割。
public class Solution {
/**
* @param m: An integer m denotes the size of a backpack
* @param A: Given n items with size A[i]
* @return: The maximum size
*/
public int backPack(int m, int[] A) {
if(A==null||A.length<=0||m<=0){
return 0;
}
//当前容量下最大值
int []dp = new int[m+1];
for (int i = 0; i < A.length; i++) {
//逆序
for (int j = dp.length-1; j >=A[i]; j--) {
dp[j] = Math.max(dp[j],dp[j-A[i]]+A[i]);
}
}
return dp[m];
}
}
这是一个01背包问题,但是不是那种特别纯的01背包问题,这个背包没有价值,最终的目的是我们装的越多越好,所以我们可以把重量当作他的价值。01背包主要就是选择还是不选择这个物件的问题,在代码中我们先对物品的重量进行遍历,然后再对容量进行遍历,dp[j]表示当容量为j时,能够装得最满的量。对应的状态转移方程如下:
d p [ j ] = { d p [ j ] ( 不 选 择 A [ i ] ) d p [ j − A [ i ] ] + A [ i ] ( 选 择 A [ i ] ) dp[j]=\left\{ \begin{aligned} dp[j](不选择A[i]) \\ dp[j-A[i]]+A[i](选择A[i]) \end{aligned} \right. dp[j]={dp[j](不选择A[i])dp[j−A[i]]+A[i](选择A[i])
其实这个状态转移方程并不难得出来,我觉得难的是为什么使用一维数组是逆序遍历,而使用二维数组是正序遍历(当然二维数组逆序也可以)。
一维数组逆序遍历是因为如果采用正序遍历的时候,开始的遍历会影响到后续的遍历结果,就拿上面的样例1为例,当第一层遍历到最后一个物品,大小为5时,第二层循环从5-10遍历,当容量为5时,更新dp[5] = 5(这时5这个大小被使用),继续遍历到容量为10时,根据公式dp[10-5]+A[3]更新dp[10]=10(这时5这个大小又被使用),打破了只能使用一次的条件,所以采用从后向前遍历的方法,成功解决这个问题。
二维数组从前正序遍历,是因为dp[i] [j]使用的是上一轮遍历的结果,从下图的箭头也可以看出,这一轮的遍历根本不会对他产生影响。图来自https://www.cnblogs.com/mfrank/p/10533701.html这篇文章
有 n
个物品和一个大小为 m
的背包. 给定数组 A
表示每个物品的大小和数组 V
表示每个物品的价值.
问最多能装入背包的总价值是多大?,题目地址:https://www.lintcode.com/problem/backpack-ii/description
样例 1:
输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
样例 2:
输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10
O(nm) 空间复杂度可以通过, 不过你可以尝试 O(m) 空间复杂度吗?
A[i], V[i], n, m
均为整数m
public class Solution {
/**
* @param m: An integer m denotes the size of a backpack
* @param A: Given n items with size A[i]
* @param V: Given n items with value V[i]
* @return: The maximum value
*/
public int backPackII(int m, int[] A, int[] V) {
// write your code here
if(A==null||A.length<=0||V==null||V.length<=0||A.length!=V.length||m<=0){
return 0;
}
//容量
int []dp = new int[m+1];
for (int i = 0; i < A.length; i++) {
//逆序遍历
for (int j = dp.length-1; j >=A[i]; j--) {
dp[j] = Math.max(dp[j],dp[j-A[i]]+V[i]);
}
}
return dp[m];
}
}
这是一个01背包问题,和上题相比有了价值这个数组,但是思想一样,所以只需要一点小改动就解决了。
给出 n 个物品, 以及一个数组, nums[i]代表第i个物品的大小, 保证大小均为正数并且没有重复, 正整数 target 表示背包的大小, 找到能填满背包的方案数。
每一个物品可以使用无数次
给出四个物品, 大小为 [2, 3, 5, 7], 价值为 [1, 5, 2, 4], 和一个大小为 10 的背包. 最大的价值为 15.
package lintcode;
public class backPackIII {
/**
* @param m: An integer m denotes the size of a backpack
* @param A: Given n items with size A[i]
* @param V: Given n items with value V[i]
* @return: The maximum value
*/
public int BackPackIII(int m, int[] A, int[] V) {
// write your code here
if(A==null||A.length<=0||V==null||V.length<=0||A.length!=V.length||m<=0){
return 0;
}
//容量
int []dp = new int[m+1];
for (int i = 0; i < A.length; i++) {
//正序遍历
for (int j = A[i]; j <dp.length; j++) {
dp[j] = Math.max(dp[j],dp[j-A[i]]+V[i]);
}
}
return dp[m];
}
public static void main(String[] args) {
backPackIII b = new backPackIII();
int m=10;
int[]A = {2,3,5,7};
int[]V = {2,5,2,4};
System.out.println(b.BackPackIII(m,A,V));
}
}
这一题是一个完全背包问题,物品是无限多,因为这题是vip才能写的,所以我只能到网上搜这个题目自己写一下,需要注意的点是这里第二层循环采用正序遍历,因为物品时无穷多的。这一题还可以使用贪婪算法进行求解,就是找性价比最高的那个,我这里的实现方法是采用动态规划。
给出 n 个物品, 以及一个数组, nums[i]
代表第i个物品的大小, 保证大小均为正数并且没有重复, 正整数 target
表示背包的大小, 找到能填满背包的方案数。
每一个物品可以使用无数次
样例1
输入: nums = [2,3,6,7] 和 target = 7
输出: 2
解释:
方案有:
[7]
[2, 2, 3]
样例2
输入: nums = [2,3,4,5] 和 target = 7
输出: 3
解释:
方案有:
[2, 5]
[3, 4]
[2, 2, 3]
public class Solution {
/**
* @param nums: an integer array and all positive numbers, no duplicates
* @param target: An integer
* @return: An integer
*/
public int backPackIV(int[] nums, int target) {
// write your code here
if(nums==null||nums.length<=0||target<=0){
return 0;
}
//代表次数
int []dp = new int[target+1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
//正序
for (int j = nums[i]; j<dp.length; j++) {
dp[j] += dp[j-nums[i]];
}
}
return dp[target];
}
}
这也是一个完全背包问题,但是又有点不同,是让我们找出填满背包的方案数,我们将dp设置为次数,然后发现这也是一个有最优子结构性质的问题。
给出 n 个物品, 以及一个数组, nums[i]
代表第i个物品的大小, 保证大小均为正数, 正整数 target
表示背包的大小, 找到能填满背包的方案数。
每一个物品只能使用一次
给出候选物品集合 [1,2,3,3,7]
以及 target 7
结果的集合为:
[7]
[1,3,3]
返回2
public class Solution {
/**
* @param nums: an integer array and all positive numbers
* @param target: An integer
* @return: An integer
*/
public int backPackV(int[] nums, int target) {
// write your code here
if(nums==null||nums.length<=0||target<=0){
return 0;
}
int []dp = new int[target+1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
//逆序
for (int j = target; j>=nums[i]; j--) {
dp[j] += dp[j-nums[i]];
}
}
return dp[target];
}
}
有了上面的经验我们只需要将背包问题IV的代码的第二层遍历改为逆序就解决了
目前的问题就总结这么多,后面有再接着添加。