背包问题是动态规划非常重要的一类问题,它有很多变种,但题目万变不离其宗。我们需要抓住关键的解题思路,现将解题模板总结如下:

背包问题的定义

那么什么样的问题可以被称作为背包问题?换言之,我们拿到题目如何透过题目的不同包装形式看到里面背包问题的不变内核呢?
我对背包问题定义的理解:给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target
注意:

1、背包容量target和物品nums的类型可能是数,也可能是字符串
2、target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
3、选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合 那么对应的背包问题就是下面我们要讲的背包分类

背包问题的分类

在我看来,背包问题可以总结为三类:01背包问题、完全背包问题以及分组背包问题。
01背包问题:每个元素最多取1次。具体来讲:一共有 N 件物品,第 i(i 从 1 开始)件物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?

完全背包问题:每个元素可以取多次。具体来讲:完全背包与 01 背包不同就是每种物品可以有无限多个:一共有 N 种物品,每种物品有无限多个,第 i(i 从 1 开始)种物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?

分组背包问题:有多个背包,需要对每个背包放入物品,每个背包的处理情况与完全背包完全相同。

在完全背包问题当中根据是否需要考虑排列组合问题(是否考虑物品顺序),可分为两种情况,我们可以通过内外循环的调换来处理排列组合问题,如果题目不是排列组合问题,则这两种方法都可以使用(推荐使用组合来解决)

而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值
2、存在问题:是否存在…………,满足…………
3、组合问题:求所有满足……的排列组合

解题模板

背包问题大体的解题模板是两层循环,分别遍历物品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、分组背包:这个比较特殊,需要多重循环:外循环nums,内部循环根据题目的要求构建多重背包循环

LeetCode原题

我们通过一些例题来更好的理解背包问题

01背包

416. 分割等和子集

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

示例 1:

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

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

思路:本题要求把数组分成两个等和的子集,相当于找到一个子集,其和为 sum / 2,这个 sum / 2 就是 target(target 间接给出)。
于是转化为是否可以用 nums 中的数组合和成 target,01 背包问题(组合),外层循环为选择池 num: nums,内层循环为 target。

class Solution {public boolean canPartition(int[] nums) {// 01背包问题int le = nums.length;int sum = 0;for(int num : nums)sum += num;if(sum % 2 == 1 || le == 1)return false;int target = sum/2;boolean[] dp = new boolean[target + 1];dp[0] = true;   // target等于0时为truefor(int num : nums) {for(int i = target; i >= num; i--) {dp[i] |= dp[i - num];}}return dp[target];}
}

复杂度分析

  • 时间复杂度:O(target × n),其中 n 是数组 nums 的长度。
  • 空间复杂度:O(target)。

494. 目标和

给你一个整数数组 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

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

思路:我们想要的 S = 正数和 - 负数和 = x - y
而已知 x 与 y 的和是数组总和:x + y = sum
可以求出 x = (S - sum) / 2 = target
也就是我们要从 nums 数组里选出几个数(组合问题),令其和为 target(target 间接给出)。
注意:负数组的和为X,这里不能用正数组,因为target可能是负数

class Solution {public int findTargetSumWays(int[] nums, int target) {int le = nums.length;int sum = 0;for(int num: nums) {sum += num;}// 如果sum<target,即都为正数也不能组成target,则直接返回0if(sum < target || (sum - target) % 2 == 1)return 0;int X = (sum - target) / 2;     // 负数组的和为X,这里不能用正数组,因为target可能是负数int[] dp = new int[X + 1];dp[0] = 1;for(int num : nums) {for(int i = X; i >= num; i--) {dp[i] = dp[i] + dp[i - num];    // 负数组选num(dp[i-num])和不选num(dp[i])的数量相加为dp[i]}}return dp[X];}
}

复杂度分析

  • 时间复杂度:O(target × n),其中 n 是数组 nums 的长度。
  • 空间复杂度:O(target)。

完全背包

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。

示例 2

输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回true 因为 “applepenapple” 可以由 “apple” “pen” “apple” 拼接成。
注意:你可以重复使用字典中的单词。

提示

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅有小写英文字母组成
  • wordDict 中的所有字符串 互不相同

思路:转化为是否可以用 wordDict 中的词组合成 s,完全背包问题(排列),外层循环为 target ,内层循环为选择池 wordDict。
dp[i] 表示以 i 结尾的字符串是否可以被 wordDict 中组合而成。

  • 外层遍历 s 中每一个与 word 同长度的字串s.substring(i - wordLen, i) ;
  • 内层遍历 wordDict 每个 word。

判断 s.substring(i - wordLen, i).equals(word):

  • 若不相等,说明与该 word 不匹配,继续遍历;
  • 若相等,说明从 i - wordLen 到 i的字符与 word 匹配。

dp[i] = dp[i] || dp[i - wordLen];
对于边界条件,我们定义 dp[0] = true 表示空串且合法。
最后返回 dp[len];

class Solution {public boolean wordBreak(String s, List<String> wordDict) {int len = s.length();// 根据字符串s建dp数组boolean[] dp = new boolean[len + 1];dp[0] = true;for(int i = 1; i <= len; i++) {for(String word : wordDict) {int wordLen = word.length();   if(i - wordLen >= 0 && s.substring(i - wordLen, i).equals(word))      dp[i] |= dp[i - wordLen];                   }}return dp[len];}
}

复杂度分析:
时间复杂度:O(target × n),其中 n 是数组 nums 的长度。
空间复杂度:O(target)。

279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

思路:我们想要的 S = 若干个完全平方数的和
完全平方数最小为 1,最大为 sqrt(n)
也就是我们要从 nums = [1, 2, …, sqrt(n)] 数组里选出几个数,令其平方和为 target = n。
于是转化为是否可以用 nums 中的数组合和成 target,完全背包问题(最值问题),外层循环为选择池 nums,内层循环为 target。
**注意:**本题是最值问题,所以不用考虑排列组合,内target外nums和内nums外target都可行。

class Solution {public int numSquares(int n) {int[] dp = new int[n + 1];Arrays.fill(dp, n + 1);dp[0] = 0;for(int i = 1; i <= n; ++i) {for(int j = 1; j * j <= i; ++j) {dp[i] = Math.min(dp[i], dp[i - j * j] + 1);}}return dp[n];}
}

复杂度分析:

  • 时间复杂度:O(n x sqrt{n}),在主步骤中,我们有一个嵌套循环,其中外部循环是 n 次迭代,而内部循环最多需要 sqrt{n} 迭代。
  • 空间复杂度:O(n),使用了一个一维数组 dp。

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 。

提示:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000

思路:转化为是否可以用 coins 中的数组合和成 amount,完全背包问题(组合),外层循环为选择池 coins,内层循环为 amount。
对于元素之和等于 i - coin 的每一种组合,在最后添加 coin 之后即可得到一个元素之和等于 i 的组合,因此在计算 dp[i] 时,应该计算所有的 dp[i − coin] 之和。

dp[i] = dp[i] + dp[i - coin]

class Solution {public int change(int amount, int[] coins) {int[] dp = new int[amount + 1];dp[0] = 1;for(int coin: coins) {for(int i = coin; i <= amount; ++i) {dp[i] += dp[i - coin];}}return dp[amount];}
}

复杂度分析:

  • 时间复杂度:O(amount x n),其中 n 为 coins 大小
  • 空间复杂度:O(amount)

多组背包

474. 一和零
给你一个二进制字符串数组 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 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 ‘0’ 和 ‘1’ 组成
  • 1 <= m, n <= 100

思路:
计算出strs中每个值的0和1个数
然后用dp来计算(01背包问题,只是这里是二维,即装两个背包)
使用空间优化

class Solution {public int findMaxForm(String[] strs, int m, int n) {int length = strs.length;int[][] dp = new int[m + 1][n + 1];for (int i = 1; i <= length; i++) {int[] zerosOnes = getZerosOnes(strs[i - 1]);int zeros = zerosOnes[0], ones = zerosOnes[1];for (int j = m; j >= zeros; j--) {for (int k = n; k >= ones; k--) {dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1);}}}return dp[m][n];}public int[] getZerosOnes(String str) {int[] zerosOnes = new int[2];int length = str.length();for (int i = 0; i < length; i++) {zerosOnes[str.charAt(i) - '0']++;}return zerosOnes;}
}

复杂度分析:

  • 时间复杂度:O(lmn+L),遍历strs,其中求出zero和one,然后mn
  • 空间复杂度:O(mn),空间优化后为mn
  • l为strs的长度,L为strs中所有字符串总长度

背包问题大全(动态规划)相关推荐

  1. 【基础】一叶知秋,从背包问题到动态规划

    如果想了解更多内容,欢迎关注我的微信公众号:信息学竞赛从入门到巅峰. 之前我们讲解了几种典型背包问题的问题模型和解决方法. 其实,背包问题只是动态规划问题(简称DP)的冰山一角.动态规划还包含众多类型 ...

  2. 01背包问题的动态规划求解及其C++实现

    本文讲解01背包问题的动态规划求解,并使用C++进行了实现 文章目录 01背包问题 动态规划 01背包问题的动态规划求解 01背包问题的动态规划求解-C++实现 01背包问题 有nnn个物品,这些物品 ...

  3. 动态规划之0/1背包问题(动态规划入门)

    动态规划很早以前就接触过但是因为太晦涩难懂一下子到现在才开始真正的学习到其中的道理,0/1背包问题是动态规划的入门类问题 比较好理解 首先我们要知道动态规划是用于解决最优解的问题 它是一种思想而不是一 ...

  4. 变种 背包问题_动态规划入门——传说中的零一背包问题

    今天是周三算法与数据结构专题的第12篇文章,动态规划之零一背包问题.在之前的文章当中,我们一起探讨了二分.贪心.排序和搜索算法,今天我们来看另一个非常经典的算法--动态规划.在acm-icpc竞赛领域 ...

  5. 八十八、从斐波那契数列和零一背包问题探究动态规划

    @Author:Runsen 编程的本质来源于算法,而算法的本质来源于数学,编程只不过将数学题进行代码化. ---- Runsen 本人看了vivo,阿里巴巴的校招算法题,可以明确知道绝对有动态规划. ...

  6. 1008-----算法笔记----------0-1背包问题(动态规划求解)

    1.问题描述 给定n种物品和一个背包,物品i的重量是wi,其价值为vi,背包的容量为C.问:应该如何选择装入背包的物品,使得装入背包中物品的总价值最大? 2.问题分析 上述问题可以抽象为一个整数规划问 ...

  7. 01背包问题,动态规划求解

    01背包问题: 1.递归思想 0- 1 背包问题如果采用递归算法来描述则非常清楚明白, 它的算法根本思想是假设用布尔函数 knap( s, n) 表示n 件物品放入可容质量为s 的背包中是否有解( 当 ...

  8. 背包问题(动态规划)

    本篇文章作为个人的背包问题学习资料,来自转载 dd大牛的<背包九讲>. P01: 01背包问题 题目 有N件物品和一个容量为V的背包.第i件物品的费用是c[i],价值是w[i].求解将哪些 ...

  9. python 完全背包问题_动态规划——背包问题python实现(01背包、完全背包、多重背包)...

    参考: 描述: 有N件物品和一个容量为V的背包. 第i件物品的体积是vi,价值是wi. 求解将哪些物品装入背包,可使这些物品的总体积不超过背包流量,且总价值最大. 二维动态规划 f[i][j] 表示只 ...

  10. 从0-1背包问题到动态规划

    算法--贪心算法解0-1背包问题 局部最优策略(locally optimal decisions,比如贪心算法),并不保证全局最优(global optimums) "在我的后园,可以看见 ...

最新文章

  1. Kubernetes — 安装 Metrics Server
  2. 技师学院计算机老师,技师学院计算机教学课堂改革探索论文
  3. python函数概述_Python概述
  4. C#如何将线程中的代码抛到主线程去执行
  5. JavaScript上传图片及时预览
  6. 由中行IBM大型机宕机谈银行系统运维
  7. 信息学奥赛C++语言:陶陶摘苹果
  8. getchar吸收回车
  9. 同花顺 python量化交易_开启量化第一步!同花顺iFinD数据接口免费版简易操作教程...
  10. MMA8452Q 三轴加速度传感器驱动
  11. 详解冯诺依曼体系结构与操作系统
  12. MIT License探讨
  13. pymol作图-输出PNG格式图片
  14. 视频目标检测入门介绍
  15. Django 千锋培训读书笔记
  16. SQL Server 2008 Service Pack 1 - CTP 发布
  17. rnn 循环神经网络
  18. 从另一个世界归来的幽灵
  19. 游戏测试内存泄露相关方案
  20. 2022情人节脱单相亲文档

热门文章

  1. 谷歌浏览器打开阅读清单
  2. BigDecimal 类型的金额 compareTo比较大小
  3. 紫猫插件-网络共享数据(16-18)
  4. Android 模拟吹气实现吹风车效果
  5. 云知声-AI离线语音识别芯片模块系列方案介绍
  6. 不要拿你的认知来评价别人
  7. 人工晶状体在线公式A常数优化
  8. c语言long可以存几位数,long long 可以支持多少位的数?
  9. 蛋白质组学与转录组学联合分析
  10. html源代码中 图像的属性标记,HTML图像标签img和源属性src及Alt属性、宽高、对齐...