lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

动态规划背包问题

 

1. 引言

动态规划是一种解决复杂问题的方法,它可以将一个问题分解为若干个子问题,然后利用子问题的最优解来构造原问题的最优解。动态规划的核心思想是避免重复计算,即将已经求解过的子问题的结果保存起来,以便后续使用。

背包问题是一类经典的动态规划问题,它描述了一个背包有一定的承重上限,而有若干个物品,每个物品有自己的重量和价值,如何选择装入背包的物品,使得背包内物品的总价值最大。背包问题有很多变种,例如01背包、完全背包、多重背包等,它们都可以用动态规划的方法来求解。

本文将介绍动态规划背包问题的基本概念和思路,并以leetcode里的题目作为案例,给出JAVA实现的答案。本文将涵盖以下几种类型的背包问题:

  • 01背包问题:每种物品只有一个,可以选择放或不放。
  • 完全背包问题:每种物品有无限个,可以选择放任意个。
  • 多重背包问题:每种物品有有限个,可以选择放任意个但不能超过给定的数量。
  • 混合三种背包问题:每种物品可能属于以上三种情况之一。
  • 二维费用的背包问题:每种物品除了重量还有另一种费用,背包也有相应的限制。
  • 分组的背包问题:物品分为若干组,每组只能选择一个物品放入背包。
  • 有依赖的背包问题:物品之间存在依赖关系,例如要放某个物品必须先放另一个物品。

 

leetcode  上相应的练习题

  • 01背包问题:每种物品只有一个,可以选择放或不放。
    • LeetCode 416. 分割等和子集 https://leetcode-cn.com/problems/partition-equal-subset-sum/
    • LeetCode 494. 目标和 https://leetcode-cn.com/problems/target-sum/
    • LeetCode 474. 一和零 https://leetcode-cn.com/problems/ones-and-zeroes/
  • 完全背包问题:每种物品有无限个,可以选择放任意个。
    • LeetCode 322. 零钱兑换 https://leetcode-cn.com/problems/coin-change/
    • LeetCode 518. 零钱兑换 II https://leetcode-cn.com/problems/coin-change-2/
    • LeetCode 377. 组合总和 Ⅳ https://leetcode-cn.com/problems/combination-sum-iv/
  • 多重背包问题:每种物品有有限个,可以选择放任意个但不能超过给定的数量。
    • LeetCode 暂无

2.  0-1背包

2.1   题目 1  416. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。

解析

这道题可以转化为一个0-1背包问题,即从数组中选取若干个数,使得它们的和等于数组总和的一半。

如果数组总和是奇数,则直接返回false。

如果是偶数,则定义一个二维数组dp[i][j]

 

  • 二维数组有两种 定义 解法:
    • dp[i][j]表示从前i个数中选取若干个数,使得它们的和不超过j的最大值, 则状态转移方程为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]] + nums[i])
    • dp[i][j]表示前i个数能否组成和为j的子集,它是一个布尔值,如果为true,表示可以组成,如果为false,表示不可以组成。状态转移方程  dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]]
 

解法一 参照 :7-012-(LeetCode- 416) 分割等和子集 

 

解法二:如下

二维数组dp[i][j]表示从前i个数中选取若干个数,使得它们的和不超过j的最大值。则状态转移方程为:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]] + nums[i])

其中,dp[i-1][j]表示不选第i个数,dp[i-1][j-nums[i]] + nums[i]表示选第i个数。最后判断dp[n][sum/2]是否等于sum/2即可。

代码

class Solution {
    public boolean canPartition(int[] nums) {
        // 数组总和
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 如果总和是奇数,则不能分割成两个相等的子集
        if (sum % 2 == 1) {
            return false;
        }
        // 数组长度
        int n = nums.length;
        // dp数组
        int[][] dp = new int[n + 1][sum / 2 + 1];
        // 初始化第一行为0
        for (int j = 0; j <= sum / 2; j++) {
            dp[0][j] = 0;
        }
        // 遍历物品
        for (int i = 1; i <= n; i++) {
            // 遍历容量
            for (int j = 0; j <= sum / 2; j++) {
                // 不选第i个数
                dp[i][j] = dp[i - 1][j];
                // 如果容量大于等于第i个数,可以选择选或不选
                if (j >= nums[i - 1]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - nums[i - 1]] + nums[i - 1]);
                }
            }
        }
        // 判断是否能分割成两个相等的子集
        return dp[n][sum / 2] == sum / 2;
    }
}

复杂度

  • 时间复杂度:O(n * sum),其中n是数组长度,sum是数组总和,需要遍历二维dp数组。
  • 空间复杂度:O(n * sum),需要创建一个二维dp数组。

 

 

 

2.2   题目 2   LeetCode 494. 目标和 https://leetcode-cn.com/problems/target-sum/

题目:

给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

解法:参照 7-013-(LeetCode- 494) 目标和 

 

2.3   题目 3  LeetCode 474. 一和零 https://leetcode-cn.com/problems/ones-and-zeroes/

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

动态规划-01背包问题 :474. 一和零 

 

  

3.   完全背包

3.1 题目1:LeetCode 322. 零钱兑换 https://leetcode-cn.com/problems/coin-change/

 
 

3.2 题目2:动态规划 完全背包问题 -游戏最大伤害

和  LeetCode 322  是类似的一道题

 

3.3  题 目3:LeetCode 518. 零钱兑换 II

7-010-(LeetCode- 518) 零钱兑换II

3.4  题目 518. 零钱兑换 II

 

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = 2 输出:0 解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 输出:1

解析

这道题可以转化为一个完全背包问题,即从coins数组中选取若干个数,使得它们的和等于amount。定义一个一维数组dp[j]表示容量为j的背包可以装入的硬币组合数。则状态转移方程为:

dp[j] = dp[j] + dp[j - coins[i]]

其中,dp[j]表示不选第i种硬币,dp[j - coins[i]]表示选第i种硬币。注意这里是加号,因为要求所有可能的组合数。最后返回dp[amount]即可。

代码

class Solution {
    public int change(int amount, int[] coins) {
        // dp数组
        int[] dp = new int[amount + 1];
        // 初始化
        dp[0] = 1;
        // 遍历物品
        for (int coin : coins) {
            // 遍历容量,正序
            for (int j = coin; j <= amount; j++) {
                // 状态转移方程
                dp[j] += dp[j - coin];
            }
        }
        // 返回结果
        return dp[amount];
    }
}

复杂度

时间复杂度:O(n * amount),其中n是coins数组长度,amount是总金额,需要遍历一维dp数组。

空间复杂度:O(amount),需要创建一个一维dp数组。

 

3.5  题目4:LeetCode 377. 组合总和 Ⅳ https://leetcode-cn.com/problems/combination-sum-iv/

   动态规划-背包问题-完全背包问题:leetcode 377. 组合总和 Ⅳ 

 

4.   多重背包

4.1  题目  322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3 输出:-1

示例 3:

输入:coins = [1], amount = 0 输出:0

解析

这道题可以转化为一个多重背包问题,即从coins数组中选取若干个数,使得它们的和等于amount,且每种硬币有有限个。定义一个一维数组dp[j]表示容量为j的背包可以装入的最少硬币个数。则状态转移方程为:

dp[j] = min(dp[j], dp[j - coins[i]] + 1)

其中,dp[j]表示不选第i种硬币,dp[j - coins[i]] + 1表示选第i种硬币。注意这里是最小号,因为要求最少的硬币个数。最后判断dp[amount]是否等于初始值,如果是则返回-1,否则返回dp[amount]。

代码

class Solution {
    public int coinChange(int[] coins, int amount) {
        // dp数组
        int[] dp = new int[amount + 1];
        // 初始化为最大值
        Arrays.fill(dp, Integer.MAX_VALUE - 1);
        // 初始条件
        dp[0] = 0;
        // 遍历物品
        for (int coin : coins) {
            // 遍历容量,正序
            for (int j = coin; j <= amount; j++) {
                // 状态转移方程
                dp[j] = Math.min(dp[j], dp[j - coin] + 1);
            }
        }
        // 返回结果,判断是否有解
        return dp[amount] == Integer.MAX_VALUE - 1 ? -1 : dp[amount];
    }
}

复杂度

时间复杂度:O(n * amount),其中n是coins数组长度,amount是总金额,需要遍历一维dp数组。

空间复杂度:O(amount),需要创建一个一维dp数组。

5.  总结

背包问题是一类常见的动态规划问题,它的核心思想是定义一个合适的状态表示和状态转移方程,然后根据问题的要求进行初始化和遍历。在解决背包问题时,需要注意以下几点:

  • 根据物品的数量和重复性选择合适的背包类型,如0-1背包、完全背包或多重背包。
  • 根据问题的要求选择合适的状态属性,如最大值、最小值、方案数或具体方案。
  • 根据状态转移方程选择合适的遍历顺序,如正序或逆序。
  • 根据状态压缩的可能性选择合适的空间优化方法,如降维或滚动数组。

6.  参考链接

posted on 2023-06-28 14:17  白露~  阅读(1635)  评论(1编辑  收藏  举报