23/9/12刷题记录之动态规划

本文探讨了背包理论的01背包问题,使用动态规划方法,涉及二维数组、滚动数组实现及解题模板,同时涉及最值问题和数组优化策略。
摘要由CSDN通过智能技术生成

一、 背包理论基础

参考:动态规划:01背包理论基础
在这里插入图片描述
一篇文章吃透背包问题!(细致引入+解题模板+例题分析+代码呈现

1. 问题分类

  1. 0/1背包问题:每个元素最多选择一次
  2. 完全背包问题:每个元素可以重复选择
  3. 组合背包问题:背包中的物品要考虑顺序
  4. 分组背包问题:不止一个背包,需要遍历每个背包
    而每个背包问题要求也是不同的,按照所求问题分类:
  5. 最值问题
  6. 存在问题
  7. 组合问题

2. 01 背包之二维数组

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维数组的01背包问题

  1. 确定dp数组以及下标的定义:即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 在这里插入图片描述
  2. 确定递推公式:
    从下标为[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])
  1. 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];
}
  1. 确定遍历顺序
    虽然两个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]);
	}
}	
  1. 举例
    在这里插入图片描述
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背包之滚动数组

  1. 确定dp数组以及下标的定义
    在一维数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
  2. 一维数组dp的递推公式
    dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  3. 初始化
    假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了
  4. 遍历顺序
    二维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]);
	}
}
  1. 举例
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

  1. 0/1背包:
    外循环 nums,
    内循环 target,
    target 倒序且 target>=nums[i];
  2. 完全背包:
    外循环nums,
    内循环target,
    target正序且target>=nums[i];
  3. 组合背包:外循环target,内循环nums,target正序且target>=nums[i];
  4. 分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板

问题分类的模板

  1. 最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)
    或 dp[i] = max/min(dp[i], dp[i-num]+nums);
  2. 存在问题(bool):dp[i]=dp[i] || dp[i-num];
  3. 组合问题: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部曲:
  1. 确定dp数组以及下标的定义
    这题是01背包,容量为j的背包,所背的物品价值最大可以为dp[j]。但是本题目中,每一个元素的数值是重量,也是价值。
    所以,dp[j]指背包总容量j,放进物品后,最大的重量为dp[j]
  2. 递推方程
    dp[j] = max(dp[j], dp[j - nums[i]] + nums[i] )
  3. 初始化
    dp[0] = 0
  4. 遍历顺序
    0-1背包的顺序
  5. 举例
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【中等】
本题求背包最多可以装多少

  1. 确定dp数组以及其下标的含义:dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]
  2. 确定递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
  3. dp数组初始化:dp[j]都初始化为0就可以了
  4. 遍历顺序:外层物品,内层倒序背包
//一维数组
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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花花橙子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值