算法---- 01背包问题和完全背包问题LeetCode系列问题题解

Posted TheWhc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法---- 01背包问题和完全背包问题LeetCode系列问题题解相关的知识,希望对你有一定的参考价值。

背包问题:

  • 背包:最大容量v
  • 物品:
    • 物品价值w
    • 物品体积v
    • 每个物品的数量
      • 只有一个(01背包)
      • 无数个(完全背包)

例子:

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

背包最大重量为4。

物品为:

重量价值
物品0115
物品1320
物品2430

问背包能背的物品最大价值是多少?

1、01背包入门

1.1 二维背包

/**
 * 背包最大重量为4
 *
 *        重量    价值
 * 物品0    1     15
 * 物品1    3     20
 * 物品2    4     30
 *
 *
 */

/**
 * 思路: 二维dp数组01背包
 *
 * 1. 确定dp数组以及下标含义
 * dp[i][j]: 表示从下标为0~i的物品任意取,放进容量为j的背包,价值总和最大是多少
 *
 * 2. 确定递推公式
 * if(j < weight[i]) {
 *    dp[i][j] = dp[i-1][j];
 * } else {
 *        dp[i][j] = Math.max(dp[i-1][j-weight[i]] + value[i], dp[i-1][j])
 *                       放入物品i                        不放入物品i
    * }
 *
 * 3. 初始化
 * 背包容量为0时, 即j = 0 时, dp[i][0] = 0
 *
 * i = 0时
 * for(int j = weight[0]; j <= bagweight; j++) {
 *     dp[0][j] = value[0];
 * }
 *
 * 4. 确定遍历顺序
 * 先遍历物品,再遍历背包容量; 反之也可以
 * 从上到下,从左往右
 *
 * 5. 举例推导dp数组
 *        重量
 *         0   1  2  3  4
 * 物品0    0  15 15 15 15
 * 物品1    0  15 15    20 35
 * 物品2    0  15 15 20 35
 *
 *
 * 时间: O(n*m)
 * 空间: O(n*m)
 * n为物品,m为重量
 */
static void wei_bag_problem() {
   int[] weight = {1,3,4};
   int[] value = {15,20,30};
   int bagWeight = 4;

   // 1. 确定dp数组以及下标含义
   // 二维数组
   // dp[i][j] 代表下标[0-i]物品里任意取,放进容量为j的背包,价值总和最大是多少
   int[][] dp = new int[weight.length][bagWeight + 1];

   // 2. 初始化
   // 背包重量为0的 dp[i][0]价值为0
   for (int i = 0; i < weight.length; i++) {
      dp[i][0] = 0;
   }

   // 存放编号为0的物品的时候, 各个容量的背包所能存放的最大价值
   // 倒叙遍历
   /*for (int j = bagWeight; j >= weight[0]; j--) {
      dp[0][j] = dp[0][j - weight[0]] + value[0];
   }*/

   // 正序遍历
   for (int j = weight[0]; j <= bagWeight; j++) {
      dp[0][j] = value[0];
   }

   // 3. 遍历顺序
   for (int i = 1; i < weight.length; i++) { // 遍历物品
      for (int j = 1; j <= bagWeight; j++) { // 遍历背包容量
         if(j < weight[i]) {
            dp[i][j] = dp[i - 1][j];
         } else {
            dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
         }
      }
   }

   System.out.println(dp[weight.length-1][bagWeight]);
}

1.2 一维背包

public class _01_背包_一维 {

   /**
    * 背包最大重量为4
    *
    *        重量    价值
    * 物品0    1     15
    * 物品1    3     20
    * 物品2    4     30
    *
    *
    */

   /**
    * 思路: 一维数组01背包
    *
    * 1. 确定dp数组以及下标含义
    * dp[j]: 表示容量为j的背包的最大价值
    *
    * 2. 确定递推公式
    * dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i])
    *               不放入当前物品     放入当前物品
    *
    * 3. 初始化
    *      dp[0] = 0
    *
    * 4. 确定遍历顺序
    * 先遍历物品,再遍历背包容量
    * 遍历背包容量的时候,应该是倒叙遍历
    *
    * 如果是正序遍历的话
    * dp[1] = dp[1-weight[0]] + value[0] = 15
    * dp[2] = dp[2-weight[0]] + value[0] = 15 + 15 =  30
    * 重复放了2次
    *
    * 如果是倒叙遍历的话
    * dp[2] = dp[2-weight[0]] + value[0] = 15
    * dp[1] = dp[1-weight[0]] + value[0] = 15
    *
    * 5. 举例推导递推公式
    *      背包容量:   0  1  2  3  4
    * 用物品0,遍历背包: 0 15 15 15 15
    * 用物品1,遍历背包: 0 15 15 20 35
    * 用物品2,遍历背包: 0 15 15 20 35
    */
   public static void wei_bag_problem() {
      int[] weight = {1,3,4};
      int[] value = {15, 20, 30};
      int bagweight = 4;

      // 1. 确定dp数组及下标含义
      // dp[j] 表示容量为j的背包,价值总和最大是多少
      int[] dp = new int[bagweight+1];

      // 2. 确定递推公式
      // dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) 不放入物品, 放入物品
      // 所以一维数组时,递推公式如下
      // dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i])

      // 3. 初始化
         // 要和dp数组的定义吻合
      // 若价值都为正整数,则全部初始化为0, 如果出现负整数, 那么价值要初始化为最小值(负无穷),防止比较大小的时候出错
      // j = 0时,背包容量为0

      // 4. 确定遍历顺序
      // 先遍历物品,再遍历背包重量(反之不行)
      for (int i = 0; i < weight.length; i++) {
         // 倒叙遍历
         // 从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
         // 背包容量从大到小,与二维dp写法不同(二维正序倒叙都可),倒叙遍历目的是使得物品i只被放入一次
         for (int j = bagweight; j >= weight[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i]);
         }
      }

      // (错误写法)先背包容量,后物品情况
      // 那么背包里只会放入了一个物品
      // 背包容量要倒叙遍历,防止出现重复放入物品
      /*for (int j = bagweight; j >= 0; j--) {
         for (int i = 0; i < weight.length; i++) {
            if(j >= weight[i]) {
               dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i]);
            }
         }
      }*/
   }

}

2、01背包问题

2.1 背包装最多问题

416. 分割等和子集

/**
 * 思路: _01背包
 *
 * 如何转化为01背包?
 * - 背包体积(重量)为sum/2
 * - 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
 * - 背包如何正好装满,说明找到了总和为sum/2的子集
 * - 背包中里的每一个元素都是不可重复放入的
 *
 * 1. 确定dp数组以及下标含义
 * dp[j]: 表示背包容量为j,所背的物品价值可以最大为dp[j]
 *
 * 2. 确定递推公式
 * dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
 *
 * 3. 初始化
 * dp[0] = 0
 *
 * 4. 确定遍历顺序
 * 先遍历物品,再遍历背包容量
 * 注意遍历背包容量的时候应该是逆序遍历,防止重复放入
 *
 * 5. 举例推导dp数组
 * 比如nums=[1,5,11,5]
 *
 * 背包容量为11
 *     背包容量 0 1 2 3 4 5 6 7 8 9 10 11
 * 元素0       0 1 1 1 1 1 1 1 1 1  1  1
 * 元素1       0 1 1 1 1 1 6 6 6 6 6  6
 * 元素2       0 1 1 1 1 1 6 6 6 6 6  11
 * 元素3      0 1 1 1 1 5 6 6 6 6  10 11
 */
public static boolean canPartition(int[] nums) {
   int sum = 0;
   int maxNum = 0;
   for (int num : nums) {
      sum += num;
      maxNum = Math.max(maxNum, num);
   }

   if(sum % 2 == 1 || maxNum > sum / 2) {
      return false;
   }

   // 1.dp数组
   int target = sum / 2;
   int[] dp = new int[target + 1];

   // 2.确定递推公式
   // 3.初始化

   // 4.确定遍历顺序
   for (int i = 0; i < nums.length; i++) {
      // 后遍历背包容量,倒叙遍历
      for (int j = target; j >= nums[i]; j--) {
         dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
         //       不放入当前物品    放入当前物品
      }
   }

   return dp[target] == target;
}

1049. 最后一块石头的重量II

// 求背包最多能装多少

/**
 * 思路: 01背包 动态规划
 * 将石头尽量拆分成重量相同的两堆,所以最后返回结果sum - dp[重量/2] - dp[重量/2]
 *
 * 1. 确定dp数组以及下标含义
 * dp[j]表示背包容量为j的最大所背重量
 *
 * 2. 确定递推公式
 * dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i])
 *           不放入当前石头   放入当前石头
 *
 * 3. 初始化
 * dp[0] = 0
 *
 * 4. 确定遍历顺序
 * 01背包,先遍历物品,再遍历背包容量,背包容量应该逆序遍历
 *
 * 5. 举例推导dp数组
 * 输入[2,4,1,1]
 *   下标  0  1  2  3  4
 * 石头0   0  0  2  2  2
 * 石头1   0  0  2  2  4
 * 石头2   0  1  2  3  4
 * 石头3   0  1  2  3  4
 *
 * dp[4] = 4
 * res = 8 - 4 - 4 = 0
 *
 * 时间: O(n * m) n为石头块数
 * 空间: O(m)   m为石头总重量的一半
 */
public int lastStoneWeightII(int[] stones) {
   // 1 <= stones.length <= 30
   // 1 <= stones[i] <= 100
   // 最大重量的一半 = 30 * 100 / 2 = 1500
   int[] dp = new int[1501];

   int sum = 0;
   for (int stone : stones) {
      sum += stone;
   }

   int target = sum / 2;

   for (int i = 0; i < stones.length; i++) {
      for (int j = target; j >= stones[i]; j--) {
         dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i]);
      }
   }

   return sum - dp[target] - dp[target];
}

2.2 凑满背包方法数问题

494. 目标和

// 凑满背包方法数问题

/**
 * 思路: 01背包动态规划
 *
 * 如何转化为01背包?
 * 假设加法总和为x,那么减法对应的总和为sum - x
 * 所以 x - (sum - x) = target
 *  x = (target + sum) / 2
 *  所以只需要求凑满x有多少种方案就可以了
 *
 *  注意这里的除2问题,如果不能整除,那么直接返回0,没有方案
 *  还有一个问题这里是两个数相加,可能出现整数溢出,但本题不用担心溢出问题
 *
 *  1. 确定dp数组
 *  dp[j]:表示凑满j有多少种方案,注意是凑满,之前都是求容量为j时,最多能装多少
 *
 *  2. 确定递推公式
 *  dp[j] += dp[j - nums[i]]
 *  只需要搞到nums[i],凑成dp[j]就有dp[j-nums[i]]方法
 *
 *  3. 初始化
 *  dp[0] = 1 : 表示凑满背包容量为0,有1种方案,dp[0]也是一切递推结果的起源
 *
 *  4. 确定遍历顺序
 *  先遍历物品,再遍历背包容量,注意背包容量逆序遍历
 *
 *  5. 举例推导数组
 *  例子: [1,1,1,1,1]
 *    下标   0  1  2  3  4
 *  物品0    1  1  0  0  0
 *  物品1    1  2  1  0  0
 *  物品2    1  3  3  1  0
 *  物品3    1  4  6  4  1
 *  物品4    1  5  10 10 5
 *
 *  时间: O(n * m)  n为nums数组长度,m为背包容量
 *  空间: O(m)
 *
 */
public int findTargetSumWays(int[] nums, int target) {
   int sum = 0;
   for (int num : nums) {
      sum += num;
   }

   int bagSize = (target + sum) / 2;
   if((target + sum) % 2 == 1) {
      return 0;
   }

   int[] dp = new int[bagSize + 1];

   dp[0] = 1;

   // 遍历物品
   for (int i = 0; i < nums.length; i++) {
      // 遍历背包容量
      for (int j = bagSize; j >= nums[i]; j--) {
         dp[j] += dp[j - nums[i]];
      }
   }

   return dp[bagSize];
}

2.3 双重维度问题

474. 一和零

/**
 * 思路: 01背包问题
 *
 * 如何转化为01背包?
 * m和n相当于是一个背包,两个维度的背包
 *
 * 一个维度是m,一个维度是n,不同长度的字符串就是不同大小的待装物品
 *
 * 1. 确定dp数组以及下标含义
 * dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
 *
 * 2. 确定递推公式
 * dp[i][j] = Math.max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1)
 *             不放入当前字符串    放入当前字符串(由前一个str里的字符串推出来,所以减去当前字符串的0和1的个数得到前一个字符串的最大子集再加1)
 *
 * 3. 初始化
 * 初始化都为0
 *
 * 4. 确定遍历顺序
 * 先遍历物品(strs)
 * 再遍历背包
 
 * 能不能先遍历背包,再遍历物品?
	 * 因为这里物品只能存放一次,所以背包容量应该是倒叙遍历,防止重复放置
	 * 并且应该是先遍历物品,再遍历背包容量,如果是先遍历背包容量,后遍历物品时,那么就会出现背包只会存放一个元素的情况
 *
 */
public int findMaxForm(String[] strs, int m, int n) {
   int[][] dp = new int[m+1][n+1];

   // 外层遍历物品
   for (String str : strs) {
      int zeroSum = 0;
      int oneSum = 0;
      for (int i = 0; i < str.length(); i++) {
         if(str.charAt(i) == '0') {
            zeroSum++;
         } else {
            oneSum++;
         }
      }

      // 内层遍历背包容量,逆序遍历
      for (int i = m; i >= zeroSum; i--) {
         for (int j = n; j >= oneSum; j--) {
            dp[i][j] = Math.max(dp[i][j], dp[i-zeroSum][j-oneSum] + 1);
         }
      }
   }

   return dp[m][n];
}

3、完全背包问题

3.1 凑满组合问题

518. 零钱兑换II

// 完全背包 + 凑满背包问题 + 组合数,求凑满背包的个数

/**
 * 思路: 完全背包
 * 1. 确定dp数组以及下标含义
 * dp[j]: 凑成总金额为j的货币组合数为dp[j]
 *
 * 2. 确定递推公式
 * dp[j](考虑coins[i]的组合总和),就是所有的dp[j-coins[i]]相加
 *
 * dp[j] += dp[j-coins[i]]
 *
 * 3. 初始化
 * dp[0] = 1是一切递推的基础
 *
 * 4. 确定遍历顺序
 * 外层遍历物品,内层遍历背包容量(正序遍历)
 *
 * 能否先遍历背包容量(正序遍历),再遍历物品?
 * 答案是不行的,因为本题是求组合问题,不是求排列问题,排列问题才需要考虑元素之间的顺序问题
 *
 * 比如先遍历物品,再遍历背包容量,假设coins[0]=1,coins[1]=5
 * 得到的方法数量只有{1,5},不会出现{5,1}情况
 *
 * 但是如果先遍历背包容量,再遍历物品,假设coins[0]=1,coins[1]=5
 * 得到的方法数量既{1,5},也有{5,1}情况
 *
 * 5. 举例推导dp数组
 *  amount = 5, coins = [1, 2, 5]
 * 背包容量  0 1 2 3 4 5
 * 物品0    1 1 1 1 1 1
 * 物品1    1 1 2 2 3 3
 * 物品2    1 1 2 2 3 4
 */
public int change(int amount, int[] coins) {
   int[] dp = new int[amount+1];

   dp[0] = 1;

   for (int i = 0; i < coins.length; i++) {
      for (int j = coins[i]; j <= amount; j++) {
         dp[j] += dp[j-coins[i]];
      }
   }

   return dp[amount];
}

3.2 凑满排列问题

377. 组合总和IV

// 完全背包 + 凑满问题 + 排列数, 求组成target元素组合的个数

/**
 * 思路: 完全背包
 *
 * 1. 确定dp数组以及下标含义
 * dp[j]: 凑成目标数为j的排列数为dp[j]
 *
 * 2. 确定递推公式
 * dp[j] += dp[j-nums[i]]
 * 求凑满元素的个数问题,都是这样的公式
 *
 * 3. 初始化
 * dp[0] = 1,是一切递推的前提
 *
 * 4. 确定遍历顺序
 *  求排列数,先遍历背包容量,再遍历物品
 *  因为是完全背包,所以背包容量应该正序遍历
 *  因为是排列问题,所以外层是背包容量
 *
 *  5. 举例推导递推公式
 *  nums = [1,2,3], target = 4
 *   背包容量 0 1 2 3 4
 *  物品0    1 1 1 2 4
 *  物品1    1 1 2 3 6
 *  物品2    1 1 2 4 7
 */
public int combinationSum4(int[] nums, int target) {
   int[] dp = new int[target+1];

   dp[0] = 1;

   // 凑满排列问题,外层先遍历背包容量
   for (int j = 1; j <= target; j++) {
      // 内存再遍历物品
      for (int i = 0; i < nums.length; i++) {
         if(j >= nums[i]) {
            dp[j] += dp[j-nums[i]];
         }
      }
   }

   return dp[target];
}

JZ9. 跳台阶扩展问题

/**
 * 思路: 完全背包问题
 *
 * 如何转换?
 * 比如跳到阶梯3时,凑成3时, 可以是1、2步,也可以是2、1步,这两种方法不同, 即求的是排列数
 *
 * 1. 确定dp数组
 * dp[j]: 凑成目标target的排列数为dp[j]
 *
 * 2. 确定递推公式
 * dp[j] += dp[j-nums[i]]
 *
 * nums[i]的取值范围为[1,target]
 *
 * 3. 初始化
 * dp[0] = 1
 *
 * 4. 确定遍历顺序
 * 凑满排列问题,先遍历背包容量,再遍历物品
 * 因为是元素可以重复选,所以背包容量正序遍历
 *
 * 5. 举例推导dp数组
 * 输入3
 *        0 1 2 3
 * 物品0  1 1 1 2
 * 物品1  1 1 2 3
 * 物品2  1 1 2 4
 */
public int jumpFloorII(int target) {

   int[] dp = new int[target+1];

   dp[0] = 1;

   // 外层遍历背包容量
   for (int j = 1; j <= target; j++) {
      // 内层遍历物品,即阶梯[1,target]
      for (int i = 1; i <= target; i++) {
         if(j >= i)
            dp[j] += dp[j-i];
      }
   }

   return dp[target];
}

3.3 凑满最少问题

322. 零钱兑换

// 求凑成总金额所需的最少的硬币个数

// 完全背包 + 凑满问题 + 凑满最少问题
/**
 * 思路: 完全背包
 * 1. 确定dp数组以及下标含义
 * dp[j]: 凑成金额为amount的最少硬币数为dp[j]
 *
 * 2. 确定递推公式
 * dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1)
 *             不放入当前硬币   放入当前硬币
 *
 * 3. 初始化
 * dp[0] = 0
 * 凑成金额为0的硬币数一定为0
 * 其它dp应该初始化为最大值,这样才能保证不被覆盖
 *
 * 4. 确定遍历顺序
 * 先遍历物品,再遍历背包容量
 * 或者先遍历背包容量,再遍历物品都可以(因为是最少硬币数,不是求排列数)
 *
 * 由于是求凑满问题,所以背包容量依然是正序遍历,保证元素可以被重复选取
 *
 * 5. 举例推导dp数组
 * coins = [1, 2, 5], amount = 5
 *   背包容量  0 1 2 3 4 5
 * 物品0      0 1 2 3 4 5
 * 物品1      0 1 1 2 2 3
 * 物品2      0 1 1 2 2 1
 *
 * 最少硬币数为1
 *
 */
public int coinChange(int[] coins, int amount) {
   int[] dp = new int[amount+1];

   for (int i = 1; i <= amount; i++) {
      dp[i] = Integer.MAX_VALUE;
   }

   // 金额为0时需要的硬币数目为0
   dp[0] = 0;

   // 外层遍历物品
   for (int i = 0; i < coins.length; i++) {
      // 内层遍历背包容量
      // 正序遍历,保证物品可以重复放入
      for (int j = coins[i]; j <= amount; j++) {
         // 注意这里只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要,否则会发现整数溢出
         // 因为有可能出现无法用给定的硬币凑成目标数的情况,比如{2},amount=3
         if(dp[j-coins[i]] != Integer.MAX_VALUE)
            dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1);
      }
   }

   // 返回的时候,需进行一下判断是不是可以凑成
   return dp[amount] != Integer.MAX_VALUE ? dp[amount] : -1;
}

279. 完全平方数

// 完全背包 + 凑满问题 + 凑满最少问题

/**
 * 思路: 完全背包问题
 *
 * 如何转化为背包问题?
 * 因为求的是凑成完全平方数的问题,所以凑成目标数当作背包容量,而凑成的完全平方数字看成物品,无限使用
 *
 * 1. 确定dp数组以及下标含义
 * dp[j]: 表示凑成和为j的完全平方数的最少数量dp[j]
 *
 * 2. 确定递推公式
 * dp[j] = Math.min(dp[j], dp[j-i*i] + 1)
 *
 * if(j >= i * i) {
 *     dp[j] = Math.min(dp[j], dp[j-i*i] + 1)
 * }
 * 背包j的取值范围为[0,n]
 * 物品i的取值范围为[1, i*i<=n]
 *
 * 3. 初始化
 * dp[0] = 0
 * 其余都初始化为最大值,防止被覆盖
 *
 * 4. 确定遍历顺序
 * 先遍历物品,再遍历背包容量(正序遍历,物品可以重复放入)
 * 或者先遍历背包容量,再遍历物品都

以上是关于算法---- 01背包问题和完全背包问题LeetCode系列问题题解的主要内容,如果未能解决你的问题,请参考以下文章

算法---- 01背包问题和完全背包问题LeetCode系列问题题解

算法---- 01背包问题和完全背包问题LeetCode系列问题题解

dp背包问题/01背包,完全背包,多重背包,/coin change算法求花硬币的种类数

01背包问题和完全背包问题

动态规划算法-07背包问题进阶

动态规划算法-07背包问题进阶