组合总和

力扣题目链接(opens new window)

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

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

说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。

示例 1:

  • 输入:candidates = [2,3,6,7], target = 7,
  • 所求解集为: [ [7], [2,2,3] ]

示例 2:

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

思路

组合总和III 不同的点在于:

  • 单个组合内元素个数没有限制,只要不超过target即可
  • 由于上述原因,本题中递归的层数也没有限制

基于此,我们就不能用组合的size()作为终止条件了,并且在单层处理逻辑中,还需要使用beginIndex来确定遍历起始位置

题外话:何时需要beginIndex

如果是一个集合来求组合的话,就需要beginIndex;(例如组合、组合总和III)

如果是多个集合取组合,各个集合之间相互不影响,那么就不用beginIndex;(电话号码)

回到正题

本题的基本框架与组合总和III完全一致,下面我就写的简略一些

1、确定回溯函数参数与返回值

仍然需要定义两个数组用于保存路径值与结果

class Solution {
private:
//定义结果变量
vector<int> path;
vector<vector<int>> res;
//确定回溯函数参数与返回值
void backtracking(vector<int>& candidates, int target, int beginIndex){ }
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) { }
};

2、确定终止条件

前面的分析提到,本题无法再使用path的大小来作为终止条件

但我们仍可以分析出需要终止的情况:

  • target为0,说明已经找到满足条件的组合
  • target小于0,说明过了,需要停止(在深度层面来说)
class Solution {
private:
//定义结果变量
vector<int> path;
vector<vector<int>> res;
//确定回溯函数参数与返回值
void backtracking(vector<int>& candidates, int target, int beginIndex){
//确定终止条件
if(target < 0){//因为不限制单个组合中元素个数,因此递归深度也是不限的,当target < 0说明已经过了
return;
}
if(target == 0){//找到目标组合
res.push_back(path);
return;
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) { }
};

3、确定单层处理逻辑

这里大部分与之前的题一样,但是有个关键点是:进入下一层递归时,不要跳过当前值(即使用 i 而不是 i+1 ),因为可以在组合中出现重复值

完整代码

class Solution {
private:
//定义结果变量
vector<int> path;
vector<vector<int>> res;
//确定回溯函数参数与返回值
void backtracking(vector<int>& candidates, int target, int beginIndex){
//确定终止条件
if(target < 0){//因为不限制单个组合中元素个数,因此递归深度也是不限的,当target < 0说明已经过了
return;
}
if(target == 0){//找到目标组合
res.push_back(path);
return;
}
//确定单层处理逻辑
for(int i = beginIndex; i < candidates.size(); ++i){
//累减,满足条件时为0
target -= candidates[i];
path.push_back(candidates[i]);//保存路径值
backtracking(candidates, target, i);//注意不要跳过当前值
target += candidates[i];//回溯
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0);//从0开始
return res;
}
};

注意点

1、单层处理中,进入下一层递归时不要跳过当前值(并且别再的把beginIndex写到backtracking里了,应该写i才对)

2、主函数中,beginIndex应该初始化为0

组合总和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]
]

思路

组合总和组合总和III 不同

本题提供的输入数组中,元素是有重复值的。并且仍然要求我们搜索出的组合不能有重复的

区别总结如下:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而 组合总和 是无重复元素的数组candidates

重点和难点就在于如何去重

而且就算明白了需要去重,我们还要考虑在哪去重比较合适。这时候还是需要引入树结构

(因为比较复杂懒得画了,直接用卡哥的图)

在整体结构上与之前的树基本一致

不同的是这次引入了一个used数组来标记使用过的元素

先以第一条线为例,从上往下

第一层递归我们取[1,1,2]中的第一个1,标记used为[1,0,0],此时组合为[1],不满足条件

然后在第二层递归中,根据beginIndex的指向我们取[1,2]中的1,标记used为[1,1,0]。

此时组合为[1,1],不满足条件,且再往下取就非法了

因此在第二层我们可以取[1,2]中的2,标记used为[1,0,1]。

此时组合为[1,2],满足条件

再看第二条线,第一层递归我们取[1,1,2]中的第二个1,标记used为[0,1,0],有没有发现问题

因为candidates在搜索前就已经排好序,所以取第一个1的情况下回包含第二个1的所有组合

因此如果我们从第一条线搜索完毕,然后回溯到开头再从第二个1再次进入递归的话,后面搜索得到的组合一定会有重复值(与第一条线中的搜索结果重复)

例如,以第二个1开启递归的话也会得到组合[1,2]

由此我们可以发现:我们需要去重的位置应该是在单层递归时,也就是所谓的“树层”中,而在一次递归搜索的不同深度中可以使用相同的元素

上述过程中还有两个很重要的点:

1、标记数组used的状态

​ 通过used的状态可以判断是否在单层中使用了重复元素

(下面代码中再细说)

2、candidates需要提前排好序

​ 排序之后,两个重复元素会变成相邻元素。靠前的那个相邻元素的搜索结果会包含靠后那个元素的所有搜索结果

代码分析

基本框架还是一样的,就只是单层处理逻辑那不同

1、确定回溯函数的参数和返回值

输入参数不用说:candidates数组(需要先排好序)、目标值target、used数组和beginIndex

返回值不需要

class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& candidates, int target, vector<bool>& used, int beginIndex){ }
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { }
};

2、确定终止条件

和之前一样,因为这里递归深度是不限的,并且组合元素个数也是不限的

所以不可以用path的大小作为终止条件(详见 组合总和

class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& candidates, int target, vector<bool>& used, int beginIndex){
//确定终止条件
if(target < 0){
return;
}
if(target == 0){
res.push_back(path);
return;
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { }
};

3、确定单层处理逻辑

在这里要去重了

首先我们要判断当前遍历的元素是否是相邻相同元素(因此candidates需要先被排序)

然后我们要判断used数组的状态

代码如下:

class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& candidates, int target, vector<bool>& used, int beginIndex){
//确定终止条件
if(target < 0){
return;
}
if(target == 0){
res.push_back(path);
return;
}
//确定单层处理逻辑
for(int i = beginIndex; i < candidates.size(); ++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;//相邻元素,且满足used[i - 1] == 0,那么之后会出现重复值,因此要跳过
}
target -= candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, used, i);
//回溯
target += candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { }
};

解释:

关于used[i - 1] == 0

还是拿前面那张图为例

第一条线,used的变化是:[0,0,0]->[1,0,0]->[1,1,0]/[1,0,1]

这个过程中,没有涉及到重复的组合

第一条线结束后,回溯至最开始的位置,然后开始走第二条线

因为[1,1,2]中,两个1是相邻重复元素,所以在第二条线中used为[0,1,0](即used[i - 1] == 0)就意味着之后会产生重复组合了(注意,必须同时满足当前元素是相邻重复元素且used[i - 1] == 0才符合可能出现重复组合的情况)

也就是说,在有两个相邻元素时(必须满足这个条件),如果used数组中出现元素1,那么1之前不能是0

例如这里第二条线还是取1,此时的i=1,并且used[i - 1] = used[0] = 0,并且确实两个相邻的1就是重复的,因此通过used可以判断出重复元素

再举一个例子,如果这里的candidates是[1,2,2],那么当i=1,也就是选取第一个2时,此时虽然也满足used[i - 1] == 0

但是第一个2的相邻元素是1,因此不构成重复元素条件,以此类推,到第二个2时,就满足了重复条件

此时,我们需要跳过满足上述情况的遍历值

代码

在使用回溯处理数组前,一定要先对数组进行排序

class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& candidates, int target, vector<bool>& used, int beginIndex){
//确定终止条件
if(target < 0){
return;
}
if(target == 0){
res.push_back(path);
return;
}
//确定单层处理逻辑
//注意,遍历条件还多了一个target >= 0
for(int i = beginIndex; i < candidates.size() && target >= 0; ++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;//相邻元素,且满足used[i - 1] == 0,那么之后会出现重复值,因此要跳过
}
target -= candidates[i];
path.push_back(candidates[i]);
used[i] = true;//记录使用过当前值
backtracking(candidates, target, used, i + 1);//跳过当前值,因为每个数字在一个组合中只能用一次(相同值的算不同的数字)
//回溯
used[i] = false;
target += candidates[i];
path.pop_back();
} }
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
//排序
sort(candidates.begin(), candidates.end());
//初始化use数组
vector<bool> used(candidates.size(), false);
backtracking(candidates, target, used, 0);
return res;
}
};
注意点

1、单层处理逻辑中的for循环条件

​ 除了当前遍历的指针要在数组candidates大小范围内(i < candidates.size()),还要增加对于递归深度的约束条件,即target >= 0

​ 因为我们是通过target自减来寻找满足条件的组合的,如果target都减到小于0了,那可能非法了,也就不需要再遍历当前非法的递归层了

2、是否跳过当前值

​ 在单层处理逻辑调用递归时,如果允许在组合中出现重复元素,就不用跳过当前值

3、感觉解释得有点乱,如果再刷到有新的问题和理解再补充吧

​ TBD

【LeetCode回溯算法#04】组合总和I与组合总和II(单层处理位置去重)的更多相关文章

  1. 【LeetCode回溯算法#07】子集问题I+II,巩固解题模板并详解回溯算法中的去重问题

    子集 力扣题目链接 给你一个整数数组 nums ,数组中的元素 互不相同 .返回该数组所有可能的子集(幂集). 解集 不能 包含重复的子集.你可以按 任意顺序 返回解集. 示例 1: 输入:nums ...

  2. 【LeetCode回溯算法#extra01】集合划分问题【火柴拼正方形、划分k个相等子集、公平发饼干】

    火柴拼正方形 https://leetcode.cn/problems/matchsticks-to-square/ 你将得到一个整数数组 matchsticks ,其中 matchsticks[i] ...

  3. leetcode回溯算法--基础难度

    都是直接dfs,算是巩固一下 电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合. 给出数字到字母的映射如下(与电话按键相同).注意 1 不对应任何字母. 思路 一直 ...

  4. 【LeetCode回溯算法#06】复原IP地址详解(练习如何处理边界条件,判断IP合法性)

    复原IP地址 力扣题目链接(opens new window) 给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式. 有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 ...

  5. 【LeetCode回溯算法#08】递增子序列,巩固回溯算法中的去重问题

    递增子序列 力扣题目链接(opens new window) 给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2. 示例 1: 输入:nums = [4,6,7,7] ...

  6. 【LeetCode回溯算法#10】图解N皇后问题(即回溯算法在二维数组中的应用)

    N皇后 力扣题目链接(opens new window) n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击. 给你一个整数 n ,返回所有不同的 n 皇 ...

  7. LeetCode通关:连刷十四题,回溯算法完全攻略

    刷题路线:https://github.com/youngyangyang04/leetcode-master 大家好,我是被算法题虐到泪流满面的老三,只能靠发发文章给自己打气! 这一节,我们来看看回 ...

  8. LeetCode:回溯算法

    回溯算法 这部分主要是学习了 labuladong 公众号中对于回溯算法的讲解 刷了些 leetcode 题,在此做一些记录,不然没几天就忘光光了 总结 概述 回溯是 DFS 中的一种技巧.回溯法采用 ...

  9. LeetCode刷题191203 --回溯算法

    虽然不是每天都刷,但还是不想改标题,(手动狗头 题目及解法来自于力扣(LeetCode),传送门. 算法(78): 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集). 说明: ...

  10. C#LeetCode刷题-回溯算法

    回溯算法篇 # 题名 刷题 通过率 难度 10 正则表达式匹配   18.8% 困难 17 电话号码的字母组合   43.8% 中等 22 括号生成   64.9% 中等 37 解数独   45.8% ...

随机推荐

  1. Linux 查找并且复制部分文件到其他目录的办法(find xargs {})

    最近经常需要从某些文件夹查找部分文件,然后复制到其他目录里面进行进一步的处理 shell 脚本一直在不断的学习中, 最近发现之前看文档还是有疏漏. find . -iname "*fi*&q ...

  2. Springboot actuator的简单使用

    Springboot actuator的简单使用 简介 公司基于springboot研发的系统,开发已经默认集成了actuator 为了安全起见这个插件模式是不开启的. 今天与研发同事进行了沟通,简单 ...

  3. JDK内嵌指令的简单学习

    java 可以使用 java -jar的方式启动服务 日常工作中用到的比较少 javac 可以将.java 文件编译成 .class中间代码 这个工具开发编写代码中是经常需要使用的, jenkins ...

  4. Dubbo架构设计与源码解析(一) 架构设计

    作者:黄金 一.架构演变 单应用架构 ----> 垂直架构 ----> 分布式架构 ----> 微服务架构 ----> 云原生架构 二.Dubbo总体架构 1.角色职能 • C ...

  5. 一种读取亿级doris数据库的方法

    工作中,常常需要将线上doris同步至集市.读取doris数据同读取常规mysql基本相同.如果数据行小于千万,比较简单的方式直接单节点连接.读取和存储.Python示例如下: def get_dat ...

  6. VictoriaMetrics 1.84.0发布

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 11.25日,valyala大神发布了VictoriaMe ...

  7. 基于spring security创建基本项目框架

    SpringBoot建项目步骤 建表 新建项目 (package name可以自定义,整个项目只能在该包下) 选择可能有到的依赖 (别忘了勾选SQL中的Mybatis Framework,创建项目 如 ...

  8. 【JS 逆向百例】猿人学系列 web 比赛第二题:js 混淆 - 动态 cookie,详细剖析

    逆向目标 猿人学 - 反混淆刷题平台 Web 第二题:js 混淆,动态 cookie 目标:提取全部 5 页发布日热度的值,计算所有值的加和 主页:https://match.yuanrenxue.c ...

  9. 手撕Vuex-实现共享数据

    经过上一篇章介绍,完成了添加全局 $store,接下来就是实现共享数据的功能. 在 Vuex 中,共享数据是通过 state 来实现的,所以我们需要在 Nuex.js 文件中实现 state 的功能. ...

  10. 从嘉手札<2024-1-10>

    冬月初零 年岁缭绕 秋月无影 倏尔迢迢 暗章难牧 纵使再怎么保有年少飞扬的内心 时光仍带去了我二十六年的光阴 出乎意料的收到了很多人的祝福 可喜的是 仍有不少人记挂着我 于我而言 无疑是莫大的荣幸和欣 ...