一、组合总和问题

最近在看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. vue学习笔记 十二、通过计算属性获取定义的状态数据

    系列导航 vue学习笔记 一.环境搭建 vue学习笔记 二.环境搭建+项目创建 vue学习笔记 三.文件和目录结构 vue学习笔记 四.定义组件(组件基本结构) vue学习笔记 五.创建子组件实例 v ...

  2. webpack4中hash、chunkhash和contenthash三者的区别

    https://blog.csdn.net/bubbling_coding/article/details/81561362

  3. centos7_Lnmp编译安装

    17年面试运维岗位的时候,面试官要求输出一份lnmp编译的操作文档,于是有了如下安装nginx+php+mysql,进入正题: 准备环境 环境:centos7.3 软件:nginx-1.12.1 + ...

  4. 云计算&虚拟化 技术名词汇总

    云计算&虚拟化 技术名词汇总 目录 云计算&虚拟化 技术名词汇总 虚拟化方向 QEMU/qemu VMM virtual machine monitor (虚拟机监管器) Hyperv ...

  5. Introduction to DFT

    服务器使用 登陆服务器:输入账号密码 打开terminal,保证至少一个terminal窗口是打开的 取消Linux操作系统的屏幕保护 设置Linux EDA工具配置 // 自定义环境变量设置 gvi ...

  6. 使用Swagger,在编写配置类时报错Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null

    1.问题 Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet. ...

  7. cancal报错 config dir not found

    替换classpath中间封号两边的值

  8. [转帖]PostgreSQL数据库的版本历史及关键变化

    https://cloud.tencent.com/developer/article/2311843 举报 PostgreSQL是一个强大的开源关系型数据库,它的发展历程充满了创新和卓越的设计.让我 ...

  9. [转帖]《Linux性能优化实战》笔记(21)—— 网络性能优化思路

    一. 确定优化目标 优化前,我会先问问自己,网络性能优化的目标是什么?实际上,虽然网络性能优化的整体目标,是降低网络延迟(如 RTT)和提高吞吐量(如BPS 和 PPS),但具体到不同应用中,每个指标 ...

  10. [转帖]MySQL ALTER TABLE: ALTER vs CHANGE vs MODIFY COLUMN

    https://www.cnblogs.com/pachongshangdexuebi/p/5029152.html ALTER COLUMN 语法: ALTER [COLUMN] col_name ...