关于01背包的学习

什么是01背包问题?

有 N 件物品和一个最多能被重量为 W 的背包。第i件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

解题思路:

  • 确定 dp 数组及下标的含义,
  • 定义一个数组dp[ i ] [ j ]
  • i为当前物品的编号,j为当前背包能容纳的质量,dp[i][j] 表示背包容量为 j 时,能获得的最大价值。
  • 确定递推公式:
  • 根据第一步 dp 数组的定义可知,当我们遍历到第 i 个物品时,可以选择将第 i 个物品装入背包,也可以选择不装,这两种情况分别所对应的最大价值如下:
  • 当weight[ i ]<=j时装入:dp[ i ] [ j ] = dp[ i-1] [ j - weight[i]] + value[i] —— 如果装入第 i 个物品,则在装入之前需要背包先空出第 i 个物品的容量 weight[i],此时背包所获的的最大价值为 dp[ i ][j - weight[i]],然后再加上第 i 个物品的价值 value[i]
  • 当weight[ i ]>j时不装:dp[ i ][ j ] = dp[ i-1 ][ j ] —— 如果不装入第 i 个物品,就用除第 i 个物品之外的其它物品填满容量为 j 的背包,此时的最大价值就是 dp[ i-1 ][ j ]
  • 最终 dp[ i] [ j ] 取装入或者不装入第 i 个物品所获得的价值中最大的那个,即dp[ i ] [ j ] = max(dp[ i-1][ j ], dp[ i-1][j - weight[i]] + value[i])
  • dp[i][j] 递推公式:
    • 如果求背包能装的最大重量:dp[ i ][ j ] = Math.max(dp[ i-1][ j ], dp[ i-1][j - weight[i]] + value[i]);
    • 如果求装满容量为 j 的背包的方法数:dp[i][j] = dp[ i-1 ][ j ];
  • 初始化是严格按照dp 数组的定义来的,当 j 为0时,背包的容量为0,所能获取的最大价值 dp[i][0] 也是0
  • 然后开始从第一个物品开始遍历

什么题目能用01背包解决呢?

一堆东西(一组数),按照一定的方式进行组合、处理,能否凑成某种最值状态,或者凑成某种状态的方法数。能否拆分抽象成01背包的四要素:背包容量、物品编号、物品质量、物品价值。如果可以,这道题目可以使用01背包来解决。

例子:

力扣题目:分割等和子集

LeetCode链接:416. 分割等和子集

给你一个 只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100

题目分析:

假设原数组内数字总和为 sum,若分成两个子集的元素和相等,则每个子集和为 sum / 2,即“是否能从非空数组 nums 中选出一些数,这些数的和能够达到sum / 2 “。那么我们可以发现这道题目符合01背包的概念。将这道题目抽象为01背包问题:背包的容量为sum / 2,物品就是数组的元素,物品编号是数组元素的下标、物品质量为数组元素的大小、物品价值也是数组元素的大小。

解题思路:

这道题要解决的是,数组中是否能选出几个元素加起来的值等于数组元素总和的一半。

  • 定义一个boolean[ i ] [ j ]数组来存储当前sum/2=j 时,能不能选出元素来凑成sum/2.如果能boolean[ i ] [ j ]的值为true,不能为false;i表示数组元素下标,j表示sum/2为j时。
  • 如果不选取任何正整数,则被选取的元素等于 0。因此对于所有 0≤i<n,都有 dp[i][0]=true。
  • 当 i==0时,只有一个正整数 nums[0] 可以被选取,因此 dp[0][nums[0]]=true。
  • 如果 j≥nums[i],则对于当前的数字 nums[i],可以选取也可以不选取,两种情况只要有一个为 true,就有 dp[i][j]=true

  • 如果不选取 nums[i],则 dp[i][j]=dp[i-1][j]

  • 如果选取 nums[i],则dp[i][j]=dp[i−1][j−nums[i]]

  • 如果 j<nums[i]j ,则在选取的数字的和等于 j 的情况下无法选取当前的数字 nums[i],因此有dp[i][j]=dp[i−1][j]。

  • 最终得到 dp[n-1] [total]即为答案

     
    class Solution {
        public boolean canPartition(int[] nums) {
         Arrays.sort(nums);   
         int n = nums.length;
         if(n<2){
             return false;
         }
         int sum=0;
         for(int i : nums ){
         sum+=i;
         }
         int total = sum/2;
    
         if(sum%2!=0){
             return false;
         }
         
         if(nums[n-1]>total){
          return false;
         }
    
         boolean [][] dp = new boolean[n][total+1];
         for(int i=1;i<n;i++){
             dp[i][0] = true;
         }
         dp[0][nums[0]] = true;
         
         for(int i=1;i<n;i++){
             int m = nums[i];
             for(int j=1;j<=total;j++){
                 if(m<=j){
                     dp[i][j] = dp[i-1][j] | dp[i-1][j-m];
                 }else{
                     dp[i][j] = dp[i-1][j];
                 }
             }
         }
         return dp[n-1] [total];
        }
    }

 

力扣题目:最后一块石头的重量 II 

LeetCode链接:1049. 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

题目分析:

根据题目描述,当所有的石头都粉碎完时,石头的重量最小。因此可以将石头分为两堆,小的一堆、大的一堆。当小的一堆石头的重量无线接近甚至等于所有石头重量的一半的时候,剩余的石头重量最小。因此很明显,本题目符合01背包的特征。背包最大容量可以抽象为所有石头的一半质量,物品可以抽象为石头。

解题思路:

  • 定义一个维度为[n + 1][total + 1]的数组 dp[i][j] 表示当石头重量的一半为j的时候要被粉碎石头的重量,i表示为石头的编号,j表示石头重量的一半。
  • 当 i=0,没有任何石头,总重量一定是 0,对于任意 0≤j≤sum/2,dp[0][j]=0

  • 如果 j<stones[i−1],则不能选取当前石头,最大总重量为在前 i−1块石头中选取且总重量不超过 j 的情况下的最大总重量,因此最大总重量为 dp[i−1][j]

  • 如果 j≥stones[i−1],则可以不选取或选取当前石头,选择其中的最大总重量。
  • 如果不选取当前石头,则在前 i−1块石头中选取石头时总重量不超过 j,最大总重量为 dp[i−1][j]
  • 如果选取当前石头,则在前 i−1块石头中选取石头时总重量不超过 j−stones[i−1],前 i−1块石头的最大重量为 dp[i−1][j−stones[i−1]],当前石头的重量为 stones[i−1],最大总重量为 dp[i−1][j−stones[i−1]]+stones[i−1]。
  • 动态规划的状态转移方程如下:

    如果 j<stones[i−1],则 dp[i][j]=dp[i−1][j]

    如果 j≥stones[i−1],则 dp[i][j]=max⁡(dp[i−1][j],dp[i−1][j−stones[i−1]]+stones[i−1])

  • 2*dp[n][total]为粉碎石头的最大重量(粉碎石头时,两边的石头都要消耗的,所以乘2)

  • 最后sum - 2 * dp[n][total]则为最终答案

class Solution {
    public int lastStoneWeightII(int[] stones) {
    int n= stones.length;
    if(n<2){
        return stones[0];
    }
    int sum = 0;
        for (int stone : stones) {
            sum += stone;
        }

    int total = sum / 2;
    int[][] dp = new int[n + 1][total + 1];
    for (int i = 1; i <= n; i++) {
      for (int j = 0; j <= total; j++) {
                if (j < stones[i - 1]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
         dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
                }
            }
        }
        return sum - 2 * dp[n][total];
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值