组合总和 II

题目介绍

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

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

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

示例:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

问题分析

方法一

在这道问题当中我们仍然是从一组数据当中取出数据进行组合,然后得到指定的和,但是与前面的组合总和不同的是,在这个问题当中我们可能遇到重复的数字而且每个数字只能够使用一次。这就给我们增加了很大的困难,因为如果存在相同的数据的话我们就又可能产生数据相同的组合,比如在第二个例子当中我们产生的结果[1, 2, 2]其中的2就可能来自candidates当中不同位置的2,可以是第一个,可以是第三个,也可以是最后一个2。但是在我们的最终答案当中是不允许存在重复的组合的。当然我们可以按照正常的方式遍历,然后将得到的复合要求的结果加入到一个哈希表当中,对得到的结果进行去重处理。但是这样我们的时间和空间开销都会加大很多。

在这个问题当中为了避免产生重复的集合,我们可以首先将这些数据进行排序,然后进行遍历,我们拿一个数据来进行举例子:[1, 2 1],现在我们将这个数据进行排序得到的结果为:[1, 1, 2],那么遍历的树结构如下:

上图表示[1, 1, 2]的遍历树,每一个数据都有选和不选两种情况,根据这种分析方式可以构造上面的解树,我们对上面的树进行分析我们可以知道,在上面的树当中有一部分子树是有重复的(重复的子树那么我们就回产生重复的结果,因此我们要删除重复的分支,也就是不进行递归求解),如下图所示:

我们现在来分析一下上面图中产生重复子树的原因,在一层当中选1的到第二层不选1的子树和第一层不选1而第二层选1的树产生了重复,因此我们可以在第一层不选1第二层选1的子树当中停止递归。

根据上面的例子我们可以总结出来,在同一层当中,如果后面的值等于他前面一个值的话,我们就可以不去生成“选择”这个分支的子树,因为在他的前面已经生成了一颗一模一样的子树了。

现在我们的问题是如何确定和上一个遍历的节点是在同一层上面。我们可以使用一个used数组进行确定,当我们使用一个数据之后我们将对应下标的used数组的值设置为true,当递归完成进行回溯的时候在将对应位置的used值设置为false,因此当我们遍历一个数据的时候如果他前面的一个数据的used值是false的话,那么这个节点就和前面的一个节点在同一层上面。

根据上面的分析我们可以写出如下的代码:

class Solution {
vector<vector<int>> ans;
vector<int> path;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(), false);
backtrace(candidates, target, 0, 0, used);
return ans;
} void backtrace(vector<int>& candidates, int target, int curIdx,
int curSum, vector<bool>& used) {
if (curSum == target) { // 满足条件则保存结果然后返回
ans.push_back(path);
return;
} else if (curSum > target || curIdx >= candidates.size()) {
return;
}
if (curIdx == 0) {
// 选择分支
path.push_back(candidates[curIdx]);
used[curIdx] = true;
backtrace(candidates, target, curIdx + 1, curSum + candidates[curIdx], used);
// 在这里进行回溯
path.pop_back();
used[curIdx] = false;
// 不选择分支
backtrace(candidates, target, curIdx + 1, curSum, used);
}else {
if (used[curIdx - 1] == false && candidates[curIdx - 1] ==
candidates[curIdx]) { // 在这里进行判断是否在同一层,如果在同一层并且值相等的话 那就不需要进行选择了 只需要走不选择的分支及即可
backtrace(candidates, target, curIdx + 1, curSum, used);
}else{
// 选择分支
path.push_back(candidates[curIdx]);
used[curIdx] = true;
backtrace(candidates, target, curIdx + 1, curSum + candidates[curIdx], used);
// 在这里进行回溯
path.pop_back();
used[curIdx] = false;
// 不选择分支
backtrace(candidates, target, curIdx + 1, curSum, used);
}
}
}
};

方法二

在回溯算法当中我们一般有两种选择情况,这一点我们在前面组合问题当中已经介绍过了,一种方法是用选择和不选择去生成解树,这样我们将生成一颗二叉树的解树,另外一种是多叉树,下面我们来看一下后者的解树:

同样对上面的解树进行分析我们回发现同样的也存在相同子树的情况,如下图所示,途中绿色的节点就时相同的节点:

与前一种方法分析一样,当当前的数据和同一层上面的前一个数据相同的时候我们不需要进行求解了,可以直接返回跳过这个分支,因为这个分支已经在前面被求解过了。具体的分析过程如下图所示:

因此和第一种方法一样,我们也需要一个used数组去保存数据是否被访问过,但是在这个方法当中我们还可以根据遍历时候下标去实现这一点,因此不需要used数组了,代码如下所示:

Java代码

class Solution {
private List<List<Integer>> res = new ArrayList<>();
private ArrayList<Integer> path = new ArrayList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtrace(candidates, target, 0, 0);
return res;
} public void backtrace(int[] candidates, int target, int curSum,
int curPosition) {
if (curSum == target) // 达到条件则保存结果然后返回
res.add(new ArrayList<>(path));
for (int i = curPosition;
i < candidates.length && curSum + candidates[i] <= target;
i++) {
// 如果 i > curPosition 说明 i 对应的节点和 curPosition 对应的节点在同一层
// 如果 i == curPosition 说明 i 是某一层某个子树的第一个节点
if (i > curPosition && candidates[i] == candidates[i - 1])
continue;
path.add(candidates[i]);
curSum += candidates[i];
backtrace(candidates, target, curSum, i + 1);
// 进行回溯操作
path.remove(path.size() - 1);
curSum -= candidates[i];
}
} }

C++代码

class Solution {
vector<vector<int>> ans;
vector<int> path;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrace(candidates, target, 0, 0);
return ans;
} void backtrace(vector<int>& candidates, int target, int curIdx, int curSum) {
if (curSum == target) {
ans.push_back(path);
return;
} else if (curSum > target || curIdx >= candidates.size()) {
return;
}
for(int i = curIdx; i < candidates.size() && curSum + candidates[i] <= target; ++i) {
if (i > curIdx && candidates[i] == candidates[i - 1])
continue;
path.push_back(candidates[i]);
backtrace(candidates, target, i + 1, curSum + candidates[i]);
path.pop_back();
}
}
};

总结

在本篇文章当中主要给大家介绍了组合问题II,这个问题如果仔细进行分析的话会发现里面还是有很多很有意思的细节的,可能需要大家仔细进行思考才能够领悟其中的精妙之处,尤其是两种方法如何处理重复数据结果的情况。


以上就是本篇文章的所有内容了,我是LeHung,我们下期再见!!!更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

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

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

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

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

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

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

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

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

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

  5. 40组合总和II

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

  6. 040 Combination Sum II 组合总和 II

    给定候选号码数组 (C) 和目标总和数 (T),找出 C 中候选号码总和为 T 的所有唯一组合.C 中的每个数字只能在组合中使用一次.注意:    所有数字(包括目标)都是正整数.    解决方案集不 ...

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

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

  8. LeetCode(40):组合总和 II

    Medium! 题目描述: 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的每个数 ...

  9. leetcode第40题:组合总和II

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

随机推荐

  1. Mvcapi解决H5请求接口跨域问题

    using Newtonsoft.Json;using System;using System.Collections.Generic;using System.Linq;using System.N ...

  2. 2022-07-15/16 第一小组 田龙月 管理系统javaSE

    JavaSE小项目(基础语法:二分查找:冒泡排序)--还是存在bug:删除一个数组内一组数据后面只有一组后面数据能向前移位 (YY:使用"方法"应该会好很多,代码架构会清晰一点)未 ...

  3. 数码管动态显示Verilog实现(参考小梅哥教程)(视觉暂留)

    一个数码管有九个引脚,控制八段二极管的亮灭,用以显示需要的数字. 当有N个数码管时,一个一个控制的话需要N x 9 个引脚,消耗资源较多. 因此可以利用动态显示的方案通过人眼的视觉暂留特性达到静态显示 ...

  4. super和访问权限修饰符

    super()方法 构造方法中的super关键字 在java子类的构造方法中可以通过super关键字来调用父类的构造方法 super():访问父类中的无参构造函数 super(...)访问父类中的成员 ...

  5. 发布自己的npm组件库

    发布自己的npm组件库 前期准备 **git上建立自己的仓库,这样子是为了在本地初始化时,可以直接生成对应的 package.json ** 克隆仓库 创建仓库成功后,gitbush本地,执行 git ...

  6. 飞书前端提到的竞态问题,在 Android 上怎么解决?

    请点赞关注,你的支持对我意义重大. Hi,我是小彭.本文已收录到 GitHub · AndroidFamily 中.这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] ...

  7. Ceph创建一个新集群 报错: File "/usr/bin/ceph-deploy", line 18, in..........

    [root@ceph-node1 ceph]# ceph-deploy new node1 Traceback (most recent call last): File "/usr/bin ...

  8. kubernetes网络模型

    Overview 本文将探讨Kubernetes中的网络模型,以及对各种网络模型进行分析. Underlay Network Model 什么是Underlay Network 底层网络 Underl ...

  9. 百亿数据百亿花, 库若恒河沙复沙,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang数据库操作实践EP12

    Golang可以通过Gorm包来操作数据库,所谓ORM,即Object Relational Mapping(数据关系映射),说白了就是通过模式化的语法来操作数据库的行对象或者表对象,对比相对灵活繁复 ...

  10. MybatisPlus——全网配置最全的代码生成器

    MybatisPlus代码生成器 这里讲解的是新版 (mybatis-plus 3.5.1+版本),旧版不兼容 官方文档:https://baomidou.com/(建议多看看官方文档,每种功能里面都 ...