01背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
解题思路
如果用暴力解法,那么对于没个物品有两种情况,取或者不取,最终的时间复杂度就是2^n,这已经是指数级别的时间复杂度了,所以不能采用。
为了方便理解,这里假设 n =3 ,w=4
采用动态规划的五步分析法:
确定dp[i][j]数组的含义
表示从下标为[0-i]的物品里任意取,放进容量为j的背包,最大价值总和
确定递推公式
不放物品i时:背包容量为j,放入物品i会导致背包超重,此时dp[i][j] = dp[i-1][j]
放物品i时: 此时背包最大价值应该是不放物品i时的最大价值加上物品i的价值,即 dp[i][j] = dp[i-1][j-weight[i]] + value[i]
所以递推公式如下:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
dp数组初始化
从递推公式含义出发,初始化如下:
当j=0时,背包容量为0,此时总价值dp[i][0]一定为0;
当i=0时,j<weight[i] ,那么dp[0][j] = 0;
当i=0时,j>=weight[i],那么dp[0][j] = value[i];
其余默认初始化0
确定遍历顺序
因为递推公式是由dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。所以遍历顺序并不影响结果。既可以先遍历物品,也可以先遍历背包重量。
先遍历物品代码:
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
完整测试代码示例-java
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}
/**
* 动态规划获得结果
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods][bagSize + 1];
// 初始化dp数组
// 创建数组后,其中默认的值就是0
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}
// 填充dp数组
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i]) {
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i-1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
}
}
}
// 打印dp数组
for (int i = 0; i < goods; i++) {
for (int j = 0; j <= bagSize; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
}
一维dp数组
上面二维数组的递推公式为 dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])。
我们发现可以将dp[i-1]的那一行copy到dp[i]上,此时公式变为dp[i][j] = max(dp[1][j],dp[i][j-weight[i]]+value[i])。这个时候其实可以只用一个一维数组dp[j]来代替dp[i][j].
对于dp[j]有以下分析:
1)确定dp[j]含义
dp[j] 表示背包最大重量为j时,所能装的最大价值
2)dp[j] 的递推公式
dp[j] 可以由两种情况推导出来,第一种就是dp[j],不放物品i;另一种就是加上物品i的价值
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
3)初始化
结合定义知道,dp[0] = 0;其他也都初始化为0.
4)遍历顺序
与二维数组不同的是,当遍历背包容量时,必须从大到小遍历,否则会导致物品i重复放入。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
5)完整测试代码-java
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
解题思路
这道题可以抽象为经典的01背包问题。
首先数组nums 的元素总和如果可以平分为2,说明该总和sum是偶数。其次分成的两个子集和相等说明每个子集和target= sum/2;
于是我们可以将target看做背包最大容量,数组nums中的每个元素看做物品价值和重量。再按照求解01背包问题的方法就能加以解决了。
代码示例-java
class Solution {
public boolean canPartition(int[] nums) {
// 判空
if(nums == null ||nums.length==0){
return false;
}
int n = nums.length;
int sum = 0;
for(int i=0;i<n;i++){
sum += nums[i];
}
// 总和为奇数,不能平分
if(sum % 2!=0){
return false;
}
// 取得集合总和的一半,看做为背包总容量
int target = sum/2;
// 定义dp数组
int[] dp = new int[target+1];
// 初始化
dp[0] = 0;
// 遍历
for(int i=0;i<n;i++){
// 背包容量必须从大到小遍历
for(int j=target;j>=nums[i];j--){
// 物品i的价值和重量都是nums[i]
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[target] == target;
}
}