代码随想录算法训练营第二十五天丨 回溯算法part03

39. 组合总和

思路

题目中的无限制重复被选取,提示:1 <= candidates[i] <= 200。

本题和77.组合 (opens new window),216.组合总和III (opens new window)的区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。

本题搜索的过程抽象成树形结构如下:

注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!

回溯三部曲

  • 递归函数参数

这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。

首先是题目中给出的参数,集合candidates, 和目标值target。

target做相应的减法就可以了,最后如果 target==0 就说明找到符合的结果了。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

卡哥举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合(opens new window)

注意以上只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我在讲解排列的时候会重点介绍

代码如下:

//结果集
List<List<Integer>> res = new ArrayList<>();
//记录路径
List<Integer> path = new ArrayList<>();
void backtracking(int[] candidates, int target,int startIndex)
  • 递归终止条件

在如下树形结构中:

39.组合总和

从叶子节点可以清晰看到,终止只有两种情况,sum大于target等于target。

sum等于target的时候,需要收集结果,代码如下:

if (target == 0){// 找到了数字和为 target 的组合res.add(new ArrayList<>(path));return;
}
if (target < 0){return;
}
  • 单层搜索的逻辑

单层for循环依然是从startIndex开始,搜索candidates集合。

注意本题和77.组合 (opens new window)、216.组合总和III (opens new window)的一个区别是:本题元素为可重复选取的

如何重复选取呢,看代码,注释部分:

for (int i = startIndex; i < candidates.length; i++) {if(target-candidates[i] <0){break;}// 如果 sum + candidates[i] > target 就终止遍历path.add(candidates[i]);backtracking(candidates,target-candidates[i],i);path.remove(path.size() - 1);// 回溯,移除路径 path 最后一个元素
}

#剪枝优化

在这个树形结构中:

39.组合总和

以及上面的版本一的代码可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。

其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历

如图:

39.组合总和1

for循环剪枝代码如下:

if(target-candidates[i] <0){break;}// 如果 sum + candidates[i] > target 就终止遍历

整体代码如下:(注意注释的部分)

class Solution {//结果集List<List<Integer>> res = new ArrayList<>();//记录路径List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum(int[] candidates, int target) {Arrays.sort(candidates); // 先进行排序,利于之后递归循环进行剪枝操作backtracking(candidates,target,0);return res;}void backtracking(int[] candidates, int target,int startIndex){if (target == 0){// 找到了数字和为 target 的组合res.add(new ArrayList<>(path));return;}if (target < 0){return;}for (int i = startIndex; i < candidates.length; i++) {if(target-candidates[i] <0){break;}// 如果 sum + candidates[i] > target 就终止遍历path.add(candidates[i]);backtracking(candidates,target-candidates[i],i);path.remove(path.size() - 1);// 回溯,移除路径 path 最后一个元素}}
}

40.组合总和II

思路

这道题目和39.组合总和 (opens new window)如下区别:

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

最后本题和39.组合总和 (opens new window)要求一样,解集不能包含重复的组合。

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

用set或者map去重,这么做很容易超时!

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

这个去重为什么很难理解呢,所谓去重,其实就是使用过的元素不能重复选取。 

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

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

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

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

整体代码如下:

class Solution {//结果集List<List<Integer>> res = new ArrayList<>();//记录路径List<Integer> path = new ArrayList<>();public List<List<Integer>> combinationSum2(int[] candidates, int target) {Arrays.sort(candidates);//为了将重复的数字都放到一起,所以先进行排序backtracking(candidates,target,0);return res;}void backtracking(int[] candidates, int target,int startIndex){if (target<0){return;}if (target == 0){res.add(new ArrayList<>(path));return;}for (int i = startIndex; i < candidates.length; i++) {if (target - candidates[i] < 0){break;}//正确剔除重复解的办法//跳过同一树层使用过的元素if(i > startIndex && candidates[i] == candidates[i-1]){continue;}path.add(candidates[i]);backtracking(candidates,target-candidates[i],i+1);path.remove(path.size() - 1);}}
}

在上述中,卡哥说的使用一个used数据来标记该结点是否被使用过了。理解起来稍微有点抽象,但其实也可以不用 used 数组标记,只需要通过:

if(i > startIndex && candidates[i] == candidates[i-1]){continue;
}

来判断是否使用过该结点。具体的还是需要加深理解,具体见代码。


131.分割回文串

思路

本题这涉及到两个关键问题:

  1. 切割问题,有不同的切割方式
  2. 判断回文

回溯究竟是如何切割字符串呢?

我们来分析一下切割,其实切割问题类似组合问题

例如对于字符串abcdef:

  • 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
  • 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。

所以切割问题,也可以抽象为一棵树形结构,如图:

131.分割回文串

对于每一层遍历,先切割单个单个的字符,切割完成后判断 startIndex 是否 >= 给出的字符串长度,为true则结束当前递归,然后在逐渐切割多个。

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。

此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。

#回溯三部曲

  • 递归函数参数

全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (

本题递归函数参数还需要 startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。

代码如下:

List<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();// 放已经回文的子串
void backtracking(String s,int startIndex)
  • 递归函数终止条件

131.分割回文串

从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。

那么在代码里什么是切割线呢?

在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。

所以终止条件代码如下:

// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.length()){res.add(new ArrayList<>(path));return;
}
  • 单层搜索的逻辑

来看看在递归循环中如何截取子串呢?

for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。

首先判断这个子串是不是回文,如果是回文,就加入在 LinkedList<String> path 中,path用来记录切割过的回文子串。

代码如下:

for (int i = startIndex; i < s.length(); i++) {if (!isPalindrome(s,startIndex,i)){//如果当前不是回文子串continue;// 如果不是则直接跳过}//如果是回文子串// 获取[startIndex,i+1)在s中的子path.add(s.substring(startIndex,i+1));//字符串截取是左闭右开的backtracking(s,i+1);// 寻找i+1为起始位置的子串path.removeLast();// 回溯过程,弹出本次已经添加的子串
}

注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1

#判断回文子串

最后我们看一下回文子串要如何判断了,判断一个字符串是否是回文。

可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。

那么判断回文的C++代码如下:

boolean isPalindrome(String str,int left,int right){for (int i = left,j = right; i < j; i++,j--) {if (str.charAt(i) != str.charAt(j)){return false;}}return true;
}

整体代码如下(注释):

class Solution {List<List<String>> res = new ArrayList<>();LinkedList<String> path = new LinkedList<>();public List<List<String>> partition(String s) {backtracking(s,0);return res;}void backtracking(String s,int startIndex){// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了if (startIndex >= s.length()){res.add(new ArrayList<>(path));return;}for (int i = startIndex; i < s.length(); i++) {if (!isPalindrome(s,startIndex,i)){//如果当前不是回文子串continue;// 如果不是则直接跳过}//如果是回文子串// 获取[startIndex,i+1)在s中的子path.add(s.substring(startIndex,i+1));//字符串截取是左闭右开的backtracking(s,i+1);// 寻找i+1为起始位置的子串path.removeLast();// 回溯过程,弹出本次已经添加的子串}}boolean isPalindrome(String str,int left,int right){for (int i = left,j = right; i < j; i++,j--) {if (str.charAt(i) != str.charAt(j)){return false;}}return true;}
}

以上为我做题时候的相关思路,自己的语言组织能力较弱,很多都是直接抄卡哥的,有错误望指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/109278.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

记录:移动设备软件开发(layout六大布局)

目录 前言layoutLinearLayout线性布局LinearLayout的常用属性Android&#xff1a;orientation属性Android&#xff1a;gravity属性 TableLayout表格布局TableLayout的常用属性collapsecolumns属性shrinkcolums属性stretchcoumns属性 RelativeLayout相对布局Absolute Layout绝对…

构造shiro poc

攻击shiro思路 伪造加密过程 shiro在容器初始化的时候会实例化CookieRememberMeManager对象&#xff0c;并且设置加密解密方式 实例化时调用父类构造方法&#xff0c;设置加密方式为AES&#xff0c;并且设置key 看下调用栈 <init>:109, AbstractRememberMeManager (org…

12JVM基础

五、JVM 17、JVM基础 说一下堆栈的区别&#xff1f; 功能方面&#xff1a;堆是用来存放对象的&#xff0c;栈是用来执行程序的。 共享性&#xff1a;堆是线程共享的&#xff0c;栈是线程私有的。 空间大小&#xff1a;堆大小远远大于栈。队列和栈是什么&#xff1f;有什么区别…

python数据挖掘从入门到实战

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…

typeScript 类型什么时候使用any?

在类型不确定的情况下&#xff0c;使用 any 是一种选项&#xff0c;但通常不是最佳选择。使用 any 类型会让你失去 TypeScript 提供的类型安全性和编译时检查。以下是几种处理类型不确定性的更好方法&#xff1a; 联合类型&#xff08;Union Types&#xff09;: 如果一个值可能…

电商独立站小程序开发方案

随着移动互联网的迅速发展&#xff0c;电商行业也逐渐向小程序平台转移。开发一款电商小程序对于拓展销售渠道、提高用户体验、增加用户忠诚度等方面都有着重要的意义。本文将围绕电商小程序的开发背景、需求分析、技术选型、开发流程、风险控制、商业模式和市场前景等方面进行…

python代码调用文件或数据库中保存的脚本

这里采用的读取excel 1、先写一个测试方法 def demo5():import xlrdimport randomwb xlrd.open_workbook("code.xls")st wb.sheet_by_index(0)code st.cell_value(0, 0)list ["6666", asd, 1ad23, 1f23, 12g3, 1b3, 12r3]code2 st.cell_value(0, 1)…

[游戏开发][Unity] UnityWebRequest中断续传

UnityWebRequest和WWW加载的底层原理还是Http 断点续传的原理 UnityWebRequest第一次请求Url&#xff0c;如果请求成功&#xff0c;从头文件里把文件总长度读出来 long totalLength long.Parse(huwr.GetResponseHeader("Content-Length")) 由于文件是边下边写入的&a…

JVS规则引擎及智能BI又更新新功能啦!赶紧来试试

规则引擎更新功能 新增: 1.复合变量新增排序、排名功能 可以按照特定的顺序对数据进行排列&#xff0c;确定规则的优先级&#xff0c;可以提高数据处理效率&#xff0c;帮助分析人员更好地了解数据分布和趋势。 2.决策流新增动态日志功能 动态日志可以记录规则执行的过程和…

Jmeter性能测试(压力测试)

1.先保存 2.添加请求&#xff08;即添加一个线程组&#xff09; 3.添加取样器&#xff08;在线程组下面添加一个http请求&#xff09; 场景1&#xff1a;模拟半小时之内1000个用户访问服务器资源&#xff0c;要求平均响应时间在3000毫秒内&#xff0c;且错误率为0&#xff0…

【Linux初阶】多线程4 | POSIX信号量,基于环形队列的生产消费模型,线程池,线程安全的单例模式,STL-智能指针和线程安全

文章目录 ☀️一、POSIX信号量&#x1f33b;1.引入&#x1f33b;2.信号量的概念&#x1f33b;3.信号量函数 ☀️二、基于环形队列的生产消费模型&#x1f33b;1.理解环形队列&#x1f33b;2.代码案例 ☀️三、线程池☀️四、线程安全的单例模式&#x1f33b;1.单例模式与设计模…

如何系统性的学习报关知识

学习报关知识的方法如下&#xff1a; 学习法律法规&#xff1a;报关是受国家法律法规约束的&#xff0c;因此学习法律法规是非常重要的。可以去中华人民共和国海关总署官网查询相关法规。 学习关税分类&#xff1a;了解不同商品的分类和税率&#xff0c;可以去海关总署官网查询…

共享盘文件如何防止别人恶意删除

在如今数字化信息交流的社会中&#xff0c;共享文件已经成为很常见的设置了。然而&#xff0c;对于共享盘文件而言&#xff0c;恶意删除是一种常见的安全威胁&#xff0c;因此用户需要掌握一些方法来保护自己的文件安全。本文将介绍防止别人恶意删除共享盘文件的方法&#xff0…

mybaits动态代理实验

实验目的 掌握MyBaits动态代理的使用log4j日志的使用Lombk的使用单元测试的使用SqlSessionFactory单例模式预处理语句的使用 实验内容 完成学生表的增删改查&#xff0c;学生表信息如下 CREATE TABLE tb_student( sno INT AUTO_INCREMENT PRIMARY KEY, student_name VAR…

【ARM Coresight SoC-400/SoC-600 专栏导读】

文章目录 1. ARM Coresight SoC-400/SoC-600 专栏导读目录1.1 Coresight 专题1.1.1 Performance Profiling1.1.2 ARM Coresight DS-5 系列 1. ARM Coresight SoC-400/SoC-600 专栏导读目录 本专栏全面介绍 ARM Coresight 系统 及SoC-400, SoC-600 中的各个组件。 1.1 Coresigh…

Ubuntu系统如何配置apt远程源

查看Unbuntu版本和Codename&#xff1a; qv123localhost:~$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 22.04.3 LTS Release: 22.04 Codename: jammy # 不同的版本apt源语句中就换不同的关键字 备份文件…

logback的简单配置详解

<?xml version"1.0" encoding"UTF-8"?> <!--logback配置的根元素。scantrue表示logback将定期扫描配置文件以检测更改。scanPeriod"30 Period" 扫描间隔为30s--> <configuration scan"true" scanPeriod"30 seco…

零基础Linux_17(进程间通信)VSCode环境安装+进程间通信介绍+pipe管道mkfifo

目录 1. VSCode环境安装 1.1 使用VSCode 1.2 远程链接到Linux机器 1.3 VSCode调试 2. 进程间通讯介绍 2.1 进程间通讯的概念和意义 2.2 进程间通讯的策略和本质 3. 管道 3.1 管道介绍 3.2 匿名管道介绍 3.3 匿名管道示例代码 3.3.1 建立管道的pipe 3.3.2 匿名管道…

论文阅读:Offboard 3D Object Detection from Point Cloud Sequences

目录 概要 Motivation 整体架构流程 技术细节 3D Auto Labeling Pipeline The static object auto labeling model The dynamic object auto labeling model 小结 论文地址&#xff1a;[2103.05073] Offboard 3D Object Detection from Point Cloud Sequences (arxiv.o…

电压放大器在电子实验中有哪些作用

电压放大器在电子实验中扮演着重要的角色&#xff0c;它可以实现对电压信号的放大&#xff0c;为实验提供所需的电压级别。下面是电压放大器在电子实验中的几个常见作用&#xff1a; 信号放大&#xff1a;电压放大器的主要作用是将输入信号的幅度放大&#xff0c;以便进行更准确…