LeetCode40.组合总和II

力扣题目链接(opens new window)

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

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

说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。

  • 示例 1:
  • 输入: candidates = [10,1,2,7,6,1,5], target = 8,
  • 所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
  • 示例 2:
  • 输入: candidates = [2,5,2,1,2], target = 5,
  • 所求解集为:
[
[1,2,2],
[5]
]

思路

这道题目和[LeetCode39. 组合总和 - Tomorrowland_D - 博客园 (cnblogs.com)]()如下区别:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而[LeetCode39. 组合总和 - Tomorrowland_D - 博客园 (cnblogs.com)]()是无重复元素的数组candidates

最后本题和[LeetCode39. 组合总和 - Tomorrowland_D - 博客园 (cnblogs.com)]()要求一样,但是解集不能包含重复的组合。

本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合

  • 我们直观的可以想到以下办法:我把所有组合求出来,再用set或者map去重,这么做很容易超时!

  • 所以要在搜索的过程中就去掉重复组合。

  • 这个去重为什么很难理解呢,所谓去重,其实就是使用过的元素不能重复选取。 这么一说好像很简单!

  • 都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。

  • 那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?

  • 回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

  • 所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

  • 为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)

强调一下,树层去重的话,需要对数组排序!

1.递归的参数

  • 和Leetcode39组合一样,需要result存放结果,path存放单条路径
  • sum来存放当前的所有和
  • startindex来标志当前遍历的位置
  • 还需要一个used数组来用于去重,在下面会重点介绍去重!!!

2.递归的结束条件

  • 与上题一样,当sum>=targetSum就返回,如果等于,我们就收集结果

3.单层搜索的逻辑

这里与LeetCode39.组合总和最大的不同就是要去重了。

前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

此时for循环里就应该做continue的操作。

这块比较抽象,如图:

我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过

  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

  • 为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。

  • 而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示

单层递归的代码如下:

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}

代码:

class Solution {
public:
vector<int> path;
vector<vector<int> > result;
void backtracking(vector<int> candidates, int targetSum, int sum, int startindex, vector<bool>& used) {
if (sum >= targetSum) {
if (sum == targetSum) result.push_back(path);
return;
}
//这里的剪枝过程在组合总和中有讲到过!
for (int i = startindex; i < candidates.size() && sum + candidates[i]<=targetSum; i++) {
//如果是同一层的相同元素,就去重!也就是跳过本轮循环
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == 0) continue;
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = 1;
//要注意这里是i+1,与之前讲解的组合总和不同,这里不能够选取重复的元素
backtracking(candidates, targetSum, sum, i + 1, used);
sum -= candidates[i];
path.pop_back();
//回溯的时候将之前使用过的元素置为0,标志着这是同一层的元素(树层),而不是树枝上的元素
used[i] = 0;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear(); result.clear();
if (candidates.size() == 0) return result;
vector<bool> used(candidates.size(), 0);
//注意这里一定要排序
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};

注意:

LeetCode40.组合总和II的更多相关文章

  1. [Swift]LeetCode40. 组合总和 II | Combination Sum II

    Given a collection of candidate numbers (candidates) and a target number (target), find all unique c ...

  2. LeetCode-40. 组合总和 II C++(回溯法)

    回溯法本身是种暴力解法,虽然效率之类的比较低,但是写起来比较易懂和快.在提交之后的排名也挺低的,大概就超过8%左右.以后复习的时候再去看看题解,看看更高性能的算法.这里先暂时贴上回溯法的代码. 最后说 ...

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

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

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

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

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

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

  6. 组合总和 II

    组合总和 II 题目介绍 给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates ...

  7. LeetCode 39. 组合总和 40.组合总和II 131.分割回文串

    欢迎关注个人公众号:爱喝可可牛奶 LeetCode 39. 组合总和 40.组合总和II 131.分割回文串 LeetCode 39. 组合总和 分析 回溯可看成对二叉树节点进行组合枚举,分为横向和纵 ...

  8. 四种语言刷算法之 组合总和 II

    力扣40. 组合总和 II 1.C void back(int* candidates, int candidatesSize, int target,int start,int *path,int ...

  9. LeetCode 中级 - 组合总和II(105)

    给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的每个数字在每个组合中只能使用一次. ...

  10. 40组合总和II

    题目:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合.candidates 中的每个数字在每个组合中只能使用一 ...

随机推荐

  1. 如何使用csproj构建C#源代码组件NuGet包?

    一般我们构建传统的NuGet包,都是打包和分发dll程序集文件. 至于打包和分发C#源代码文件的做法,比较少见. 那么这种打包源代码文件的做法,有什么优点和缺点呢? 优点: 方便阅读源代码. 方便断点 ...

  2. 你了解Vim的增删改查吗 ?

    增: 在Vim的Normal模式中输入A/I/O,a/i/o字符进行对应的增加操作. 删 在Vim的Normal模式中, 输入x 删除光标对应的一个字符(4x代表删除4个字符): 输入dd删除光标所在 ...

  3. 集成学习与随机森林(四)Boosting与Stacking

    Boosting Boosting(原先称为hypothesis boosting),指的是能够将多个弱学习器结合在一起的任何集成方法.对于大部分boosting方法来说,它们常规的做法是:按顺序训练 ...

  4. NXP i.MX 8M Mini工业核心板硬件说明书(四核ARM Cortex-A53 + 单核ARM Cortex-M4,主频1.6GHz)

    1          硬件资源 创龙科技SOM-TLIMX8是一款基于NXP i.MX 8M Mini的四核ARM Cortex-A53 + 单核ARM Cortex-M4异构多核处理器设计的高端工业 ...

  5. 从零开始带你上手体验Sermant自定义插件开发

    本文分享自华为云社区<Sermant自定义插件开发上手体验>,作者:华为云开源. 一.研究缘由 由于目前我们所处的行业是汽车行业,项目上进行云服务的迁移时使用到了Sermant中的相关插件 ...

  6. C#计算两个日期的天数

    private int DateDiff(DateTime dateStart, DateTime dateEnd) { DateTime start = Convert.ToDateTime(dat ...

  7. Jenkins插件管理(Manager Plugins)【快速提升项目构建和部署实施的工作效率】

    Jenkins 是一个很棒的开源自动化平台.它有一些开箱即用的强大功能.然而,在我看来,让它脱颖而出的是它的社区和它开发的插件.有超过一千个插件可用于支持几乎所有用于构建.部署和自动化项目的技术.工具 ...

  8. 常见的SQL数值型数据处理函数

    在数据驱动的时代,SQL 已成为数据分析和管理中不可或缺的工具.无论是处理简单的查询还是复杂的数据分析,SQL 都能帮助我们高效地完成任务. 然而,在处理数值型数据时,你是否感到过困惑,不知道如何运用 ...

  9. DUILib的代码分析

    1.思路上用的是mfc的Message_MAP,而Control只是一个gdi render. 控件间使用的Message Map. 也就是说每个控件可以作为一个独立的个体

  10. python selenium 判断元素是否存在,实现:找到元素,执行对应的代码;找不到元素,继续执行其他代码

    selenium因为找不到元素会抛出异常,导致执行结束 可以考虑使用driver.find_elements(),找不到元素时就会返回空列表,使用if-else语句,判断列表是否为空,非空,则正常找到 ...