一、组合总和问题

最近在看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. 从青铜到王者,揭秘 Serverless 自动化函数最佳配置

    [福利活动]1分钟Serverless部署PHP商城实验班上线啦! 带你体验如何使用 Serverless 应用引擎 SAE 快速部署一个PHP商城,并体验 SAE 带来的弹性伸缩.应用监控等强大能力 ...

  2. 如何安全的大数据量表在线进行DDL操作

    本文为博主原创,转载请注明出处 随着业务的需要,工作中需要对生产数据库的一些表做一些DDL操作,由于生产数据库表的数据量都是几千万, 而且生产数据库的表还在不断的进行新增和查询操作.应用中需要对生产数 ...

  3. crypto常用工具

    古典密码 维吉尼亚密码(Vigenere): https://github.com/atomcated/Vigenere(加密解密程序,包含自动猜测密钥功能) https://www.guballa. ...

  4. [转帖]TiDB 整体架构

    https://docs.pingcap.com/zh/tidb/stable/tidb-architecture 与传统的单机数据库相比,TiDB 具有以下优势: 纯分布式架构,拥有良好的扩展性,支 ...

  5. 【转帖】ethool工具之TSO、UFO、GSO、LRO、GRO和RSS介绍

    ethtool -k < 网络接口>, ethtool --show-offload < 网络接口>, 或者可以看到很多网络接口的offload特性,例如: $ sudo et ...

  6. 【转帖】Linux多链路聚合技术

    https://www.jianshu.com/p/dd8587ecf54f 一般而言,在单体结构的操作系统中,一块物理磁盘会接在总线设备上,并经由总线分配 PCI-Bus 号,这个时候一个 bus  ...

  7. [转帖]使用Linux命令快速查看某一行

      原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 当年,我还是Linux菜鸟的时候,就在简历上写着精通Linux命令了,而当面试官问我"如何快速查看 ...

  8. 【验证码逆向专栏】某度滑块、点选、旋转验证码 v1、v2 逆向分析

    声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容.敏感网址.数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未经许 ...

  9. 【JS 逆向百例】网洛者反爬练习平台第一题:JS 混淆加密,反 Hook 操作

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后 ...

  10. python安装与python、pip的环境变量配置

    进入官网 在你常用的搜索引擎中输入 python官网 然后进入. 可直接点击本链接 python官网进入: 也可在浏览器地址栏输入www.python.org回车进入官网. 下载 将鼠标放到菜单栏中的 ...