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

组合总和 I

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

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。

解集不能包含重复的组合。

示例 1:

输入:candidates = [2,3,6,7], target = 7,

所求解集为:

[

[7],

[2,2,3]

]

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/combination-sum

此题要求解出所有可能的解,则需要用回溯法去回溯尝试求解,我们可以画一棵解空间树:

​ 图中绿色节点表示找到了一种可行解,而红色的节点表示到这个节点的时候组合总和的值已经大于target了,无需继续向下尝试,直接返回即可。

​ 因为题目要求解集无重复,即2,2,33,2,2应该算作同一种解,所以我们在回溯的时候应该先对candidates数组排序,然后每次只向下回溯大于等于自己的节点。

​ 观察解空间树我们发现:当某一层中第一次出现红色节点或绿色节点后,后面的节点将全变为红色,因为数组是经过排序的,任意节点后面的节点都是大于此节点的(candidates数组无重复元素),所以当出现一个红/绿色节点后,后面的节点不必再继续检查,直接剪枝即可。

剪枝后的解空间树如下:

这样看整棵解空间树就小多了,下面直接上代码:

Java版本的回溯解法代码

class Solution {

    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates,target,0,new ArrayList());
return result;
} public void dfs (int[] candidates, int target, int currSum, List<Integer> res) {
if (currSum == target) {
result.add (new ArrayList(res));
return;
}
for (int i = 0; i < candidates.length; i++) {
if (currSum + candidates[i] > target) {
return;
}
int size = res.size();
if (size==0 || candidates[i] >= res.get(size-1)) {
res.add(candidates[i]);
dfs(candidates, target, currSum+candidates[i],res);
res.remove(size);
}
}
}
}

Go版本的回溯解法代码

func combinationSum(candidates []int, target int) (result [][]int) {
sort.Ints(candidates)
var dfs func(res []int, currSum int)
dfs = func(res []int, currSum int) {
if currSum == target {
result = append(result, append([]int(nil), res...))
return
}
for i := 0; i < len(candidates); i++ {
if currSum + candidates[i] > target {
return
}
if len(res) == 0 || candidates[i] >= res[len(res)-1] {
length := len(res)
res = append(res, candidates[i])
dfs(res, currSum+candidates[i])
res = res[:length]
}
}
}
var res []int
dfs(res, 0)
return
}

组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。

解集不能包含重复的组合。

示例 1:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/combination-sum-ii

和组合总和I不同的是这个题目中的candidates数组中出现了重复数字,而且每个数字只能使用一次,我们对这个数组进行排序,每次回溯进下一层的时候都从上一层访问的节点的下一个开始访问。画出的解空间树如下:

观察解空间树发现还是有重复的解出现,比如1,2,2出现了两次,这种问题我们可以通过两种方法来解决

  1. 每次当找到一个可行解后,判断看是否此解已经存在于之前发现的解中了,如果存在就丢弃

  2. 剪枝,同一层中同样的节点只能出现一次,这样不但整个解空间树会小很多,而且避免了判断时候的开销,下面是剪枝后的解空间树

具体剪枝的方法我们可以通过增加一个visit集合,记录同一层是否出现过相同节点,如果出现过就不再次访问此节点。

我对两种解法做了对比,执行的时间效率对比如下:第一种对应上面的结果,第二种解法对应下面的结果

下面贴出第二种解法的代码:

Java版本的回溯解法代码

class Solution {

    public static void trace(List<List<Integer>> result, List<Integer> res, int[] candidates, int target, int curr, int index) {
if (curr == target) {
//得到预期目标
result.add(new ArrayList<>(res));
}
Set<Integer> visit = new HashSet<>();
for (int j = index+1; j < candidates.length; j++) {
if (visit.contains(candidates[j])) {
continue;
} else {
visit.add(candidates[j]);
}
if (curr + candidates[j] > target){
//此路不通,后路肯定也不通
break;
} else {
//继续试
res.add(candidates[j]);
int len = res.size();
trace(result, res,candidates,target,curr+candidates[j],j);
res.remove(len-1);
}
}
} public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> res = new ArrayList<>();
List<List<Integer>> result = new ArrayList<List<Integer>>();
int curr = 0;
Arrays.sort(candidates);
trace(result, res,candidates,target,curr,-1);
return result;
}
}

Go版本的回溯解法代码

func combinationSum2(candidates []int, target int) (result [][]int) {
sort.Ints(candidates)
var dfs func(res []int, currSum, index int)
dfs = func(res []int, currSum, index int) {
if currSum == target {
result = append(result, append([]int(nil), res...))
return
}
var set []int
for i := index+1; i < len(candidates); i++ {
if isExist(set, candidates[i]) {
continue
} else {
set = append(set, candidates[i])
} if currSum + candidates[i] > target { //遇到红色节点,直接跳出循环,后面也无需尝试
break
} else {
res = append(res, candidates[i])
dfs(res, currSum+candidates[i], i)
res = res[:len(res)-1]
}
}
}
var res []int
dfs(res, 0, -1)
return
} func isExist(set []int, x int) bool {
for _, v := range set {
if v == x {
return true
}
}
return false
}

组合总和 III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。

解集不能包含重复的组合。

示例 1:

输入: k = 3, n = 7

输出: [[1,2,4]]

示例 2:

输入: k = 3, n = 9

输出: [[1,2,6], [1,3,5], [2,3,4]]

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/combination-sum-iii

此题的candidates数组不再由题目给出,而是由[1,9]区间里的数组成,且每种组合不存在重复的数,则每种数字只能用一次,我们还是继续采用回溯法,不同的是限制了解集中数字的个数。而且每层的回溯都从上一层访问的节点的下一个节点开始。

如果使用暴力法去回溯,将得到下面这样的一棵解空间树(由于树过大,所以右边被省略)

因为题目中规定了树的深度必须是k,红色表示不可能的解,绿色表示可行解,紫色表示到了规定的层数k,但总和小于n的情况。

观察上述的解空间树我们发现了剪枝的方法:

  1. 对于红色节点之后的节点直接裁剪掉
  2. 但需要注意紫色的虽然不符合题意,但由于后面可能出现正确解,所以不能剪掉
  3. 根据树的深度来剪,上面两个题中都没有规定深度,此题还可以根据深度来剪,如果超过规定深度就不继续向下探索

画出剪枝后的解空间树(同样省略了右边的树结构):

Java版本的回溯解法代码

class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<Integer> res = new ArrayList<>();
List<List<Integer>> result = new ArrayList<List<Integer>>();
trace(result,res,0,k,n);
return result;
} public void trace (List<List<Integer>> result, List<Integer> res, int curr, int k, int n) {
if (res.size() == k && curr == n) {
result.add(new ArrayList<>(res));
return;
} else if (res.size() < k && curr < n) {
for (int i = 1; i < 10; i++) {
int len = res.size();
if (len == 0 || i > res.get(len - 1)) {
res.add(i);
trace(result,res,curr+i,k,n);
res.remove(len);
}
}
} else { //树的深度已经大于规定的k
return;
}
}
}

Go版本的回溯解法代码

func combinationSum3(k int, n int) (result [][]int) {
var dfs func(res []int, currSum int)
dfs = func(res []int, currSum int) {
if len(res) == k && currSum == n {
result = append(result, append([]int(nil), res...))
return
} else if len(res) < k && currSum < n {
i := 1
if len(res) > 0 {
i = res[len(res)-1]+1
}
for ; i < 10; i++ {
res = append(res, i)
dfs(res, currSum+i)
res = res[:len(res)-1]
}
} else { //搜索的深度已经超过了k
return
}
}
var res []int
dfs(res, 0)
return
}

组合总和 IV

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4

输出:7

解释:

所有可能的组合为:

(1, 1, 1, 1)

(1, 1, 2)

(1, 2, 1)

(1, 3)

(2, 1, 1)

(2, 2)

(3, 1)

请注意,顺序不同的序列被视作不同的组合。

示例 2:

输入:nums = [9], target = 3

输出:0

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/combination-sum-iv

这个道题目并没有像上面一样要求我们找出所有的解集,而是只要求解解的个数,这时如果我们再采用回溯法去求解无疑是造成了很大的浪费,所以考虑使用动态规划,只求解个数而不关注所有解的具体内容。

题目允许数字的重复,且对顺序敏感(即不同顺序视做不同解),这样我们可以通过让每一个nums数组中数num做解集的最后一个数,这样当x作为解集的最后一个数,解集就为num1,num2,num3......x

如果dp数组的dp[x]表示target为x时候的解集个数,那么我们只需要最后求解dp[target]即可。

那么当最后一个数为x时对应的解集个数就为dp[target-x]个,让nums中的每一个数做一次最后一个数,将结果相加就是dp[target]的值,不过需要注意的是dp[0] = 1表示target为0时只有一种解法(即一个数都不要),dp的下标必须为非负数。

下面是状态转移方程(n为nums最后一个元素的下标):

\[dp[i]=
\begin{cases}
1& \text{i=0}\\
\sum_{j=0}^n\ dp[target-nums[j]& \text{i!=0 && target-nums[j] > 0}
\end{cases}
\]

Java版本的动态规划解法代码

class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int num:nums) {
int tmp = i - num;
if (tmp >= 0) {
dp[i] += dp[tmp];
}
}
}
return dp[target];
}
}

Go版本的动态规划解法代码

func combinationSum4(nums []int, target int) int {
dp := make([]int, target+1)
dp[0] = 1
for i := 1; i <= target; i++ {
for _, v := range nums {
tmp := i - v
if tmp >= 0 {
dp[i] += dp[tmp]
}
}
}
return dp[target]
}

图解Leetcode组合总和系列——回溯(剪枝优化)+动态规划的更多相关文章

  1. Leetcode题目39.组合总和(回溯+剪枝-中等)

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

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

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

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

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

  4. LeetCode 组合总和(dfs)

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

  5. 40. 组合总和 II + 递归 + 回溯 + 记录路径

    40. 组合总和 II LeetCode_40 题目描述 题解分析 此题和 39. 组合总和 + 递归 + 回溯 + 存储路径很像,只不过题目修改了一下. 题解的关键是首先将候选数组进行排序,然后记录 ...

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

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

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

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

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

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

  9. [leetcode] 39. 组合总和(Java)(dfs、递归、回溯)

    39. 组合总和 直接暴力思路,用dfs+回溯枚举所有可能组合情况.难点在于每个数可取无数次. 我的枚举思路是: 外层枚举答案数组的长度,即枚举解中的数字个数,从1个开始,到target/ min(c ...

随机推荐

  1. Vue框架-组件的概念及使用

    目录 一.Vue组件 1. 组件分类 1.1 根组件 1.2 局部组件 1.3 全局组件 2. 组件的特点 3. 如何创建组件 4. 组件的数据局部化 5. 组件传参·父传子 6. 组件传参·子传父 ...

  2. Linux安装jdk(两种方式)

    最近在研究大数据方面的东西,业务场景是从设备采集数据经过处理然后存放DB. 建设上面的环境第一步肯定是安装jdk,所以和大家一起学一下基本知识centos7.5安装jdk1.8. 安装jdk有两种方法 ...

  3. 后端程序员之路 15、Matplotlib

    Matplotlib: Python plotting - Matplotlib 2.0.0 documentationhttp://matplotlib.org/ matplotlib-绘制精美的图 ...

  4. POJ-1847(SPFA+Vector和PriorityQueue优化的dijstra算法)

    Tram POJ-1847 这里其实没有必要使用SPFA算法,但是为了巩固知识,还是用了.也可以使用dijikstra算法. #include<iostream> #include< ...

  5. 10个顶级Python实用库,推荐你试试!

    为什么我喜欢Python?对于初学者来说,这是一种简单易学的编程语言,另一个原因:大量开箱即用的第三方库,正是23万个由用户提供的软件包使得Python真正强大和流行. 在本文中,我挑选了15个最有用 ...

  6. linux进程隐藏手段及对抗方法

    1.命令替换 实现方法 替换系统中常见的进程查看工具(比如ps.top.lsof)的二进制程序 对抗方法 使用stat命令查看文件状态并且使用md5sum命令查看文件hash,从干净的系统上拷贝这些工 ...

  7. 手把手教你Spring Boot2.x整合Elasticsearch(ES)

    文末会附上完整的代码包供大家下载参考,码字不易,如果对你有帮助请给个点赞和关注,谢谢! 如果只是想看java对于Elasticsearch的操作可以直接看第四大点 一.docker部署Elastics ...

  8. 使用SQLSERVER 2008 R2 配置邮件客户端发送DB数据流程要领

    设置邮件 QQ邮箱貌似不太行,建议用企业邮箱或者其他邮箱作为发件箱 新建一个邮件发件箱账号,具体邮件服务器按照各自邮件配置,是否使用ssl,自便 下一步,下一步,配置成功 use msdb Go DE ...

  9. 使用egg.js开发后端API接口系统

    什么是Egg.js Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本.详细的了解可以参考Egg.js的官网:https:// ...

  10. 为什么要从 Linux 迁移到 BSD 5

    为什么要从 Linux 迁移到 BSD 5 干净的分离 在 FreeBSD 的设计方式下,不同的组件组合在一起的,处理配置和调优,以及多年来开发和改进的所有工具,使得使用 FreeBSD 是一件很特别 ...