一、组合总和问题

最近在看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])。这个公式很抽象,表达为代码为
for (int i = 0; i <= V; i++) dp[i] = 0;//初始化二维数组
//循环每个物品
for (int i = 1; i <= n; i++)
{
for (int j = w[i]; j <= V; j++)
{
dp[j] = max(dp[j], dp[j -w[i]] + v[i]);
}
}

可以看出去掉了原始公式中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),但是因为循环的结构是这样的
for(int i=0;i<=target;i++) {
for(int j=0;j<nums.length;j++) {
// 对k做一次循环,计算dp
...
}
}

假如把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. 组合总和 Ⅳ

class Solution {
public int combinationSum4(int[] nums, int target) {
if(nums==null || nums.length ==0) {
return 0;
}
int dp[] = new int[target+1];
for(int j=0;j<nums.length;j++) {
dp[0] = 1;
}
for(int i=1;i<=target;i++) {
for(int j=0;j<nums.length;j++) {
if(i-nums[j] >= 0) {
dp[i] += dp[i-nums[j]];
}
}
return dp[target];
}
}

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

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

class Solution {
public int change(int amount, int[] coins) {
if(amount == 0) {
return 1;
}
if(amount<0) {
return 0;
}
if(coins == null || coins.length == 0) {
return 0;
} 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];
}
}

true-false问题

416. 分割等和子集

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

class Solution {
public boolean canPartition(int[] nums) {
if(nums==null || nums.length == 0) {
return false;
}
int sum = 0;
for(int i=0;i<nums.length;i++) {
sum+=nums[i];
}
if((sum & 1) == 1) {
return false;
}
int half = sum>>1; // 0-1背包
// 第i个数字, 和为j
boolean dp[] = new boolean[half+1];
dp[0] = true;
for(int i=0;i<nums.length;i++) {
for(int j=half;j>=0;j--) {
if(j>=nums[i]) {
dp[j] = (dp[j] || dp[j-nums[i]]);
}
if(j==half && dp[j]) {
return true;
}
}
}
return false;
}
}

139. 单词拆分

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

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

class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
if(s==null || s.isEmpty()) {
return true;
}
if(wordDict == null ||wordDict.size() ==0) {
return false;
} boolean dp[] = new boolean[s.length()+1];
dp[0] = true;
for(int i=0;i<=s.length();i++) {
for(int j=0;j<wordDict.size();j++) {
String wj = wordDict.get(j);
if(wj.length() <= i ) {
dp[i] = dp[i] || (dp[i-wj.length()] && wj.equals(s.subSequence(i-wj.length(),i)));
}
}
}
return dp[s.length()];
}
}

最大最小问题

  • 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来标识。

class Solution {
public int coinChange(int[] coins, int amount) {
if(amount==0) {
return 0;
}
if(amount<0 || coins==null || coins.length == 0) {
return -1;
}
int dp[] = new int[amount+1];
dp[0] = 0;
for(int i=1;i<=amount;i++) {
dp[i] = Integer.MAX_VALUE;
}
for(int i=0;i<coins.length;i++) {
for (int j=coins[i];j<=amount;j++) {
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? -1 : dp[amount];
}
}

参考文档

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

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

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. springboot线程池的使用方式1

    线程池的创建方法 总共有 7 种,但总体来说可分为 2 类: 一类是通过 ThreadPoolExecutor 创建的线程池: 另一个类是通过 Executors 创建的线程池. 1. Executo ...

  2. vue Promise的使用

    一.Promise是什么? Promise是异步编程的一种解决方案. 二.那什么时候我们会来处理异步事件呢? 1. 一种很常见的场景应该就是网络请求了. 我们封装一个网络请求的函数,因为不能立即拿到结 ...

  3. <vue 基础知识 9、v-model使用 input、radio、checkbox、select、修饰符>

    代码结构 一.     01-v-model的基本使用 Vue中使用v-model指令来实现表单元素和数据的双向绑定 1.效果 2.代码 01-v-model的基本使用.html <!DOCTY ...

  4. GPT应用开发:GPT插件开发指南

    欢迎阅读本系列文章!我将带你一起探索如何利用OpenAI API开发GPT应用.无论你是编程新手还是资深开发者,都能在这里获得灵感和收获. 本文,我们将继续展示聊天API中插件的使用方法,让你能够轻松 ...

  5. Java面试——VUE2&VUE3概览

    一.VUE2.0 1.对于MVVM的理解 MVVM 是 Model-View-ViewModel 的缩写. Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑: View 代表U ...

  6. P1955【绿】

    这道题是标准的"离散化+并查集"模版题,通过这道题彻底理解了并查集,同时还意识到了我之前一直用map来实现离散化的方法其实是最简单但是最慢的方法,以这道题为例,map导致时间消耗有 ...

  7. OpenStack 工作流组件: Mistral

    1 Mistral 简介 Mistral 是由 Mirantis 开发,贡献给 OpenStack 社区的工作流组件,它提供 Workflow As a Service 服务. 在计算机中通常处理的任 ...

  8. 每天学五分钟 Liunx 0010 | 软件篇: RPM 和 YUM

    1. RPM RPM(RedHat Package Manager),顾名思义是 RedHat 的软件包管理器.它遵循 GPL 规则且功能强大好用,从而逐渐运用到其它 Liunx 发行版中,包括 Fe ...

  9. Linux 系统安全加固经验总结

    本文为博主原创,转载请注明出处: 目录  1. 禁止root密码登录 2. linux 用户密钥复杂度及有效期设置 3. 检查sudo权限 4.关闭ftp 5.设置文件的属主并指定读写执行权限 6.管 ...

  10. 03-点亮LED灯

    1.FPGA设计流程 1.设计规划 对项目需求了解,划分子功能模块,子功能模块的输入输出信号及通信关系 2.波形绘制 了解子模块的功能,画出框图,搞清楚如何通过输入信号得到输出信号,进而绘制波形图 3 ...