一、组合总和问题

最近在看leetcode的组合问题,一共四道,总结一下共通之处与不同之处。

原题链接:

组合总和

组合总和II

组合总和III

组合总和IV

对比如下,为了便于对比,将原题目的叙述方式进行了修改。

问题 输入 取值限定 解集限定 解法
I 无重复元素的数组 candidates且全为正数;目标数 target candidates元素可以无限制重复被选取 无重复集合 回溯法,对每一个候选值可以选0~n次,满足已选之数总和小于等于target。输入无重复+回溯本身保证结果集无重复
II 可能有重复元素的数组 candidates且全为正数;目标数 target candidates元素只能选一次 无重复集合 建立candidates元素与其个数的hashmap,基于选择个数做回溯法
III candidates=[1,2,...,9],目标数 target,个数k candidates元素只能选一次,只能选k个 无重复集合 回溯法,按顺序遍历每个元素分别考虑选与不选。其他解法见原链接
IV 无重复元素的数组 candidates且全为正数;目标数 target candidates元素可以无限制重复被选取 无重复数组(顺序不同认为是不同解) 转换为背包问题的动态规划解法。先排序再用回溯法求所有无重复集合的解,最后构造结果的解法会超时。

二、背包问题

对于【组合总和IV】相关联的背包问题,做进一步的研究。

背包可以归为三类:0-1背包、完全背包、多重背包。

共性

  • 背包容量有限,求解能使背包中放下最大价值总和的金额。(本文不讨论求得最大价值总和具体放法的方式)
  • 一共n种不同的物品,对应的体积w[1...n]和价值v[1...n]
  • 求解过程是动态规划,且dp[i][j]代表【在考虑第i件物品时(无论取不取),使用空间为j时最大的价值】。那么dp[n][1...V]中最大值即为所求的最终解。(因为可能放不满)
  • 可以根据求解dp[i][j]的过程,进行存储容量压缩从而降低空间复杂度
  • 初始化dp[0][j]=0

区别

分类 输入 取值限定 解法
0-1背包 背包容量V,n种物品其体积w[1...n]和价值v[1...n] 每个物品最多取1次 见状态转移方程
完全背包 背包容量V,n种物品其体积w[1...n]和价值v[1...n] 每个物品可以取无限次 见状态转移方程
多重背包 背包容量V,n种物品其体积w[1...n]和价值v[1...n],个数分别为k[1...n] 第i个物品可以取0至k[i]次 见状态转移方程

状态转移方程

0-1背包

  • dp[i][j] = dp[i-1][j] ,当 j-w[i]<0。表示使用容量为j时,无法放下第i件,因此选择不放它
  • dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]),当 j-w[i]>=0。表示取放和不放第i件的两种情况下的最大值

优化:

  1. 观察状态转移方程时可以发现,每次都直接使用i-1行的结果来构造第i行的结果,那么只需要存储一行即可。且在遍历时,必须使用倒序遍历j->1防止本轮的变化覆盖到上一轮的结果上去,导致这一变化被再次取出来。
  2. 保存当前行的最大值,那么这个最大值在求解最后一行时即为所求的结果。

去掉i这一个维度可改写为:

  • 保持不变,当 j-w[i]<0时。
  • dp[j] = max(dp[j], dp[j-w[i]] + v[i]),当 j-w[i]>=0

完全背包

在0-1背包基础上,因为每件可以使用无限次(实际上有一个上界——不超过当前剩余容量)。公式为:

  • dp[i][j] = max(dp[i-1][j-kw[i]] + kv[i]),其中k=0,1, 2...j/w[i]取整。

但是结合0-1背包优化的过程:j倒序遍历是为了避免重复取第i个元素造成重复更新。那么反过来利用这个特性,正好能表达每个元素取无限个的特点。

那么优化公式为:

  • dp[j] = max(dp[j], dp[j-w[i]] + v[i])。这个公式很抽象,表达为代码为
  1. for (int i = 0; i <= V; i++) dp[i] = 0;//初始化二维数组
  2. //循环每个物品
  3. for (int i = 1; i <= n; i++)
  4. {
  5. for (int j = w[i]; j <= V; j++)
  6. {
  7. dp[j] = max(dp[j], dp[j -w[i]] + v[i]);
  8. }
  9. }

可以看出去掉了原始公式中k的这一层循环,并且将j的下界进行了优化,减少了判断语句。

多重背包

可以将所有类型的物品看做不同种类的,转化为0-1背包。

也可以沿着原先完全背包的思路, dp[i][j] = max(dp[i-1][j-k*w[i]] + k*v[i]),其中k=0,1, 2...k[i]取整。

这两种时间复杂度都是O(n^3)的。

有一种优化的方法是按2的幂将k件第i种物品拆分,如20=1+2+4+8+5,再使用0-1背包,可以降低至O(n^2logn)

还有更多的优化方法,可以参考 浅谈多重背包的一些解法

背包问题延伸:先遍历n个物品还是先遍历背包容量V

上文所讨论的三种背包问题基本场景,都是基于求结果的组合数的,即不考虑结果中元素的顺序,对于V=4,[1, 3]和[3, 1]是同一个解。

如果要求排列数,又如何解呢?

从上文的讨论过程可以发现,如果先按照顺序取n个/种物品再遍历背包容量V,解中第i个总在第i+1个前面,没有考虑顺序。如果先遍历容量V,再遍历元素,自然就形成了排序的解。还以V=4举例,取i=1时,V-i=3;取i=3时,V-i=1,此时可以得出出[1, 3]和[3, 1]两个不同的解。

因此,第一版的状态转移方程为:

  • dp[i][j] = Σdp[i-w[k]][j], 其中k=0...j-1,且使得i-w[k]>=0 。dp[i][j]代表占用容量为i、使用前j个元素时的组合数。如果不存在k,那么dp[i][j]=dp[i][j-1]。

    直观地看,这个复杂度是O(n^3),但是因为循环的结构是这样的
  1. for(int i=0;i<=target;i++) {
  2. for(int j=0;j<nums.length;j++) {
  3. // 对k做一次循环,计算dp
  4. ...
  5. }
  6. }

假如把dp[i]看做每一步的累加结果,即dp[i]的含义是n种物品在容量i时的摆放方式数目,这时的转移公式为:

  • dp[i] += dp[i-nums[j]],其中i-nums[j]>=0。

当然,此时的dp[i],与dp[i][j]已经不是一个含义了,dp[i]是j取最大时的dp[i][j],它的变化过程中体现了dp[i][j]。可以看出【组合问题】和原始的完全背包问题已经显现出差异。

习题求解

组合问题

377. 组合总和 Ⅳ

  1. class Solution {
  2. public int combinationSum4(int[] nums, int target) {
  3. if(nums==null || nums.length ==0) {
  4. return 0;
  5. }
  6. int dp[] = new int[target+1];
  7. for(int j=0;j<nums.length;j++) {
  8. dp[0] = 1;
  9. }
  10. for(int i=1;i<=target;i++) {
  11. for(int j=0;j<nums.length;j++) {
  12. if(i-nums[j] >= 0) {
  13. dp[i] += dp[i-nums[j]];
  14. }
  15. }
  16. return dp[target];
  17. }
  18. }

494. 目标和

可以看做元素是取正还是取反的背包问题。注意这一题进行坐标平移(+1000)和使用递推式替代状态转移方程,复杂度会更低。后者即

将 dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]] 改写为

  • dp[i][j + nums[i]] += dp[i - 1][j]
  • dp[i][j - nums[i]] += dp[i - 1][j]

    可以理解为通过上一层的基准值构造下一层的值。

    由于直接原地保存dp结果会造成干扰,优化解需要一个临时数组。

518.零钱兑换 II

典型的完全背包问题,典型的优化方式。

  1. class Solution {
  2. public int change(int amount, int[] coins) {
  3. if(amount == 0) {
  4. return 1;
  5. }
  6. if(amount<0) {
  7. return 0;
  8. }
  9. if(coins == null || coins.length == 0) {
  10. return 0;
  11. }
  12. int dp[] = new int[amount+1];
  13. dp[0] = 1;
  14. for(int i=0;i<coins.length;i++) {
  15. for (int j=coins[i]; j<=amount;j++) {
  16. dp[j] += dp[j-coins[i]];
  17. }
  18. }
  19. return dp[amount];
  20. }
  21. }

true-false问题

416. 分割等和子集

0-1背包。变化点是求固定的dp[V]是否存在(true or false)。

  1. class Solution {
  2. public boolean canPartition(int[] nums) {
  3. if(nums==null || nums.length == 0) {
  4. return false;
  5. }
  6. int sum = 0;
  7. for(int i=0;i<nums.length;i++) {
  8. sum+=nums[i];
  9. }
  10. if((sum & 1) == 1) {
  11. return false;
  12. }
  13. int half = sum>>1;
  14. // 0-1背包
  15. // 第i个数字, 和为j
  16. boolean dp[] = new boolean[half+1];
  17. dp[0] = true;
  18. for(int i=0;i<nums.length;i++) {
  19. for(int j=half;j>=0;j--) {
  20. if(j>=nums[i]) {
  21. dp[j] = (dp[j] || dp[j-nums[i]]);
  22. }
  23. if(j==half && dp[j]) {
  24. return true;
  25. }
  26. }
  27. }
  28. return false;
  29. }
  30. }

139. 单词拆分

直接套用参考文档希望用一种规律搞定背包问题

中true-false * 完全背包 问题的公式:

  1. class Solution {
  2. public boolean wordBreak(String s, List<String> wordDict) {
  3. if(s==null || s.isEmpty()) {
  4. return true;
  5. }
  6. if(wordDict == null ||wordDict.size() ==0) {
  7. return false;
  8. }
  9. boolean dp[] = new boolean[s.length()+1];
  10. dp[0] = true;
  11. for(int i=0;i<=s.length();i++) {
  12. for(int j=0;j<wordDict.size();j++) {
  13. String wj = wordDict.get(j);
  14. if(wj.length() <= i ) {
  15. dp[i] = dp[i] || (dp[i-wj.length()] && wj.equals(s.subSequence(i-wj.length(),i)));
  16. }
  17. }
  18. }
  19. return dp[s.length()];
  20. }
  21. }

最大最小问题

  • dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)

474. 一和零

二维的背包问题,限制了两个维度,因此是O(mnl)的时间复杂度。因为第一遍没想清楚,解法不粘贴了,请参考官方解。

为什么状态转移方程里有一个+1?因为取了一个新的元素,元素个数+1。

322. 零钱兑换

官方解的初始化方式理解起来不太直观,因此我直接用Integer.MAX_VALUE来标识。

  1. class Solution {
  2. public int coinChange(int[] coins, int amount) {
  3. if(amount==0) {
  4. return 0;
  5. }
  6. if(amount<0 || coins==null || coins.length == 0) {
  7. return -1;
  8. }
  9. int dp[] = new int[amount+1];
  10. dp[0] = 0;
  11. for(int i=1;i<=amount;i++) {
  12. dp[i] = Integer.MAX_VALUE;
  13. }
  14. for(int i=0;i<coins.length;i++) {
  15. for (int j=coins[i];j<=amount;j++) {
  16. if(dp[j-coins[i]] < Integer.MAX_VALUE) {
  17. dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1);
  18. }
  19. }
  20. }
  21. return dp[amount] == Integer.MAX_VALUE? -1 : dp[amount];
  22. }
  23. }

参考文档

希望用一种规律搞定背包问题

【算法总结】动态规划-背包问题

LeetCode组合总和I~IV和背包问题小结的更多相关文章

  1. 图解Leetcode组合总和系列——回溯(剪枝优化)+动态规划

    Leetcode组合总和系列--回溯(剪枝优化)+动态规划 组合总和 I 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 ...

  2. leetcode组合总和 Ⅳ 解题路径

    题目: 关于动态规划类题目的思路如何找在上一篇博客 https://www.cnblogs.com/niuyourou/p/11964842.html 讲的非常清楚了,该博客也成为了了leetcode ...

  3. 34,Leetcode 组合总和I,II -C++ 回溯法

    I 题目描述 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合.candidates 中的数字可以无 ...

  4. LeetCode 组合总和(dfs)

    题目 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的数字可以无限制重 ...

  5. Leetcode 377.组合总和IV

    组合总和IV 给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数. 示例: nums = [1, 2, 3] target = 4 所有可能的组合为: (1, 1, 1, ...

  6. 【JavaScript】Leetcode每日一题-组合总和4

    [JavaScript]Leetcode每日一题-组合总和4 [题目描述] 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target .请你从 nums 中找出并返回总和为 targ ...

  7. Leetcode之回溯法专题-216. 组合总和 III(Combination Sum III)

    Leetcode之回溯法专题-216. 组合总和 III(Combination Sum III) 同类题目: Leetcode之回溯法专题-39. 组合总数(Combination Sum) Lee ...

  8. Leetcode之回溯法专题-40. 组合总和 II(Combination Sum II)

    Leetcode之回溯法专题-40. 组合总和 II(Combination Sum II) 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使 ...

  9. LeetCode刷题笔记-回溯法-组合总和问题

    题目描述: <组合总和问题>给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. cand ...

  10. Java实现 LeetCode 40 组合总和 II(二)

    40. 组合总和 II 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的每个数字在 ...

随机推荐

  1. 阿里云 Serverless Kubernetes 的落地实践分享

    作者 | 元毅(阿里云容器平台高级开发工程师) ​ 微信搜索关注 Serverless 公众号,后台回复 深圳 可获取本文 PPT 导读** Kubernetes 作为当今云原生业界标准,具备良好的生 ...

  2. uni-app安卓手机无法连接到调试服务

    uni-app连接安卓真机,发现接口调不通,打开Hbuilder下方的调试.可查看失败原因:如下图 解决方法:电脑变热点,手机连这个热点,就能解决手机和pc在同一局域网.具体操作,参照以下网站: ht ...

  3. DDD领域驱动设计 (C# 整理自“老张的哲学”)

    大话DDD领域驱动设计 概念 Domain Driven Design 领域驱动设计 第一个D(Domain): 领域:指围绕业务为核心而划分的实体模块. 第二个D(Driven): 驱动:这里的驱动 ...

  4. java进阶(24)--ArrayList集合、LinkList集合、Vector集合

    一.基础 1.ArrayList集合底层是Object[]数组 2.默认容量10(优先:Add第一个元素,初始化未0,jdk13) 3.构造方法:无参(默认).有参 4.ArrayList集合扩容比例 ...

  5. [转帖]Linux内存之Cache

    一. Linux内存之Cache 1.1.Cache 1.1.1.什么是Cache? Cache存储器,是位于CPU和主存储器DRAM之间的一块高速缓冲存储器,规模较小,但是速度很快,通常由SRAM( ...

  6. [转帖]br备份时排除某个库

    https://tidb.net/blog/2a88149e?utm_source=tidb-community&utm_medium=referral&utm_campaign=re ...

  7. [转帖]如何在一个Docker中同时运行多个程序进程?

    https://cloud.tencent.com/developer/article/1683445 我们都知道Docker容器的哲学是一个Docker容器只运行一个进程,但是有时候我们就是需要在一 ...

  8. [转帖]tidb 如何对 TiDB 进行 TPC-C 测试

    https://docs.pingcap.com/zh/tidb/stable/benchmark-tidb-using-tpcc TPC-C 是一个对 OLTP(联机交易处理)系统进行测试的规范,使 ...

  9. [转帖]linux下/proc/sysrq-trigger详解

    /proc/sysrq-trigger详解 这是一组"魔术组合键",只要内核没有被完全锁住,不管内核在做什么事情,使用这些组合键能即时打印出内核的信息. 使用SysRq组合键是了解 ...

  10. ESXi6.7安装Win11的方法

    背景 公司里面要进行新的操作系统验证了. 之前Win10 Win7 Win8 都比较简单. 就是现在Win11有了TPM非常繁琐. 今天必须得搞一把了,就简单搜索了下. 发现还是可以解决的. 然后记录 ...