一、 背包理论基础
参考:动态规划:01背包理论基础
一篇文章吃透背包问题!(细致引入+解题模板+例题分析+代码呈现
1. 问题分类
- 0/1背包问题:每个元素最多选择一次
- 完全背包问题:每个元素可以重复选择
- 组合背包问题:背包中的物品要考虑顺序
- 分组背包问题:不止一个背包,需要遍历每个背包
而每个背包问题要求也是不同的,按照所求问题分类: - 最值问题
- 存在问题
- 组合问题
2. 01 背包之二维数组
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维数组的01背包问题
- 确定dp数组以及下标的定义:即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 确定递推公式:
从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 不放物品i:由dp[i-1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]==dp[i-1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i-1][j-weight[i]]推出,dp[i-1][j-weight[i]]为背包容量为j-weight[i]的时候不放物品i的最大价值,那么dp[i-1][j-weight[i]] + value[i](物品i的价值),就是背包放物品i得到的最大价值。
所以递推公式: dp[i][j] = max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i])
- dp数组如何初始化
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
//初始化列,通常省略了
for (int j = 0; j< weight[0];j++){
dp[0][j] = 0;
}
//初始化行,正序遍历
for(int j = weight[0];j <= bagweight;j++){
dp[0][j] = value[0];
}
- 确定遍历顺序
虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!
//weight数组的大小,就是物品个数
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]);
}
}
- 举例
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);
}
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< goods ;i++){
for(int j = weight[0]; j <= bagSize;j++){
if(j < weight[i]){
/**
当前背包的容量都没有当前物品i大的时候,是不放物品i的
那么,前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i-1][j];
}else{
/**
当前背包的容量可以放下物品i
那么有2种情况:
1、不放物品i
2、放物品i
比较这两种情况,哪种背包中的最大价值最大
*/
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
}
}
}
//打印dp数组
/**for(int[] arr : dp){
System.out.println(Arrays.toString(arr));
}*/
for(int i = 0;i < goods;i++){
for(int j = 0l j<= bagSize;j++){
System.out.print(dp[i][j] + '\t');
}
System.out.println('\n')
}
}
}
3. 01背包之滚动数组
- 确定dp数组以及下标的定义
在一维数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 - 一维数组dp的递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - 初始化
假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了 - 遍历顺序
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。因为倒序遍历是为了保证物品i只被放入一次!
for(int i = 0;i < weight.length;i++){
for(int j = bagWeight; j >= weight[i];j--){
dp[j] = max(dp[j], dp[j- weight[i] + value[i]);
}
}
- 举例
public class void main(String[] args){
int[] weight = {1,3,4};
int[] value = {15, 20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagWeight);
}
public static void testWeightBagProblem(int[] weight,int[] value,int bagWeight){
int wLen = weight.length;
//定义dp数组,dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagSize + 1];
//初始化都为0
//遍历顺序;先遍历物品,再遍历背包
for(int i = 0;i < wLen;i++){
for(int j = bagWeight; j >= weight[i];j--){
dp[j] = max(dp[j], dp[j- weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0;j <= bagSize;j++){
System.out.print(dp[j] + ' ')
}
}
4. 分类解题模板
背包分类的模板
物品是nums,背包容量是target
- 0/1背包:
外循环 nums,
内循环 target,
target 倒序且 target>=nums[i]; - 完全背包:
外循环nums,
内循环target,
target正序且target>=nums[i]; - 组合背包:外循环target,内循环nums,target正序且target>=nums[i];
- 分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板
问题分类的模板
- 最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)
或 dp[i] = max/min(dp[i], dp[i-num]+nums); - 存在问题(bool):dp[i]=dp[i] || dp[i-num];
- 组合问题:dp[i] += dp[i- num];
二、题目
剑指 Offer 10- II. 青蛙跳台阶问题【简单】
滚动一维数组
class Solution {
public int numWays(int n) {
//特殊情况
if(n < 2) return 1 % 1000000007;
//定义dp数组,当i台阶一共有几个到达的方法
int[] dp = new int[n + 1];
//递推公式 dp[i] = dp[i-1] + dp[i-2]
//初始化,0时候没意义
dp[1] = 1;
dp[2] = 2;
//遍历顺序
for(int i = 3; i < n + 1; i++ ){
dp[i] = (dp[i-1]+dp[i-2]) % 1000000007; //注意并不是只对最后的结果mod
}
return dp[n];
}
}
416. 分割等和子集【中等】
是否存在一个子集,其和为 target=sum/2,外循环nums,内循环target倒序,应用状态方程2
这个问题满足:
- 背包的体积为sum/2
- 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为sum/2的子集
- 背包中每一个元素是不可以重复放入
动态规划5部曲:
- 确定dp数组以及下标的定义
这题是01背包,容量为j的背包,所背的物品价值最大可以为dp[j]。但是本题目中,每一个元素的数值是重量,也是价值。
所以,dp[j]指背包总容量j,放进物品后,最大的重量为dp[j] - 递推方程
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i] ) - 初始化
dp[0] = 0 - 遍历顺序
0-1背包的顺序 - 举例
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0) return false;
int n = nums.length;
int sum = 0;
for(int num : nums) {
sum += num;
}
//总和为奇数,不能平分
if(sum % 2 != 0) return false;
int target = sum / 2;
int[] dp = new int[target + 1];
for(int i = 0; i < n; i++) {
for(int j = target; j >= nums[i]; j--) {
//物品 i 的重量是 nums[i],其价值也是 nums[i]
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
//剪枝一下,每一次完成內層的for-loop,立即檢查是否dp[target] == target,優化時間複雜度(26ms -> 20ms)
if(dp[target] == target)
return true;
}
return dp[target] == target;
}
}
1049. 最后一块石头的重量 II【中等】
本题求背包最多可以装多少
- 确定dp数组以及其下标的含义:dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]
- 确定递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
- dp数组初始化:dp[j]都初始化为0就可以了
- 遍历顺序:外层物品,内层倒序背包
//一维数组
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int s:stones){
sum += s;
}
int target = sum/2;
//初始化,dp[j]的意思是重量为j的时候,最大重量为dp[j]
int[] dp = new int[target+1];
for(int i = 0; i < stones.length;i++){
for(int j = target;j >= stones[i];j--){
//2种情况,放、不放
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum - 2* dp[target];
}
}
时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
空间复杂度:O(m)