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

news2025/3/13 20:01:57

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.coloradmin.cn/o/1104919.html

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

相关文章

【二层环路】交换机二次原路排查思路

以太网交换网络中为了提高网络可靠性&#xff0c;通常会采用冗余设备和冗余链路&#xff0c;然而现网中由于组网调整、配置修改、升级割接等原因&#xff0c;经常会造成数据或协议报文环形转发&#xff0c;不可避免的形成环路。如图1所示&#xff0c;三台设备两两相连就会形成环…

[解决]修复 win 32/64 位操作系统上的 PyAudio pip 安装错误

一、说明 Python3.7 无法安装pyaudio&#xff0c;度娘的结果基本都是这个&#xff0c;pip install pyaudio.....然而十有八九你的电脑不买账&#xff0c;会报错。本篇将介绍如何在win10anaconda安装pyaudio。 二、过程叙述 我有一台 Windows 10 电脑&#xff0c;我想安装 pyau…

本地jar打包成maven依赖,上传到私服

本地打包jar成maven依赖 mvn install:install-file -Dfile“\oss\xmlBeans\rvdMsgWrapper.jar” -DgroupId“hk.gov.xmlBeans” -DartifactId“noNamespace” -Dversion“1.0.0” -Dpackaging“jar” 上传到私服 登录进入到Upload 页面 上传 上传完成&#xff0c;到仓库查看…

LED路灯浪涌保护器行业应用解决方案

LED路灯是一种利用LED发光二极管作为光源的节能环保的城市道路照明设备。LED路灯具有寿命长、光效高、色温可调、无污染等优点&#xff0c;已经成为城市道路照明的主流选择。 然而&#xff0c;LED路灯也面临着一些问题&#xff0c;其中之一就是雷击浪涌的威胁。雷击浪涌是指由…

用Python做一个文件夹整理工具

文章目录 简介文件夹对话框文件映射组件完整组件 简介 我们的目的是做一个像下面这样的工具&#xff0c;前面两个输入框&#xff0c;用于输入源路径和目标路径&#xff0c;下面的图片、视频、音乐表示在目标路径中创建的文件夹&#xff0c;后面的文件后缀&#xff0c;表示将这…

HTML 表格及练习

表格 概述 表格是一种二维结构&#xff0c;横行纵列。 由单元格组成。 表格是一种非常“强” 的结构&#xff1a; 每一行有相同的列数&#xff08;单元格&#xff09;&#xff0c;每一列有相同的行数&#xff08;单元格&#xff09; 同一列的单元格&#xff0c;宽度&#…

《实验细节》使用PEFT库常见错误

《实验细节》使用PEFT库常见错误 安装问题常用命令使用问题问题1安装问题 首先给出用到的网站 更新NVIDIA网站https://www.nvidia.com/Download/index.aspx 2. 使用PEFT的优秀demo https://www.philschmid.de/fine-tune-flan-t5-peft 3. 下载一些库的必备网站 https://pypi.or…

(完全解决)latex如何设置某段文字向右对齐

开门见山&#xff0c;老子就是想要下图中日期的效果&#xff0c;可以看到&#xff0c;日期向右对齐。 很多人给的是下面这个方案&#xff1a; \begin{flushright}Sep 2020-July 2023 \end{flushright}但是试过了好像不行&#xff0c;其是换一行&#xff0c;然后向右对齐。 …

下拉选择器的树状结构图

类似&#xff1a;【Vue-Treeselect 和 vue3-treeselect】树形下拉框 一&#xff1a;图 二&#xff1a;如果有多层级的数据结构&#xff0c;可以用treeselect插件实现 1、安装&#xff1a; npm install --save riophae/vue-treeselect 2、实现&#xff1a; <el-form ref&qu…

深入了解RPA业务流程自动化的关键要素

在RPA业务流程自动化实施过程中&#xff0c;哪些因素起着至关重要的作用&#xff1f;这其实没有一个通用的答案&#xff0c;每一个RPA业务流程自动化的部署&#xff0c;都需要结合具体场景去调整&#xff0c;并且进行全面的规划。 首当其冲是要关注以下几点&#xff1a; 1、专…

想提高工作效率?这里有五款实用工具推荐

​ 想提高工作效率&#xff1f;这里有五款实用工具推荐&#xff01;搜索一下就能下载到。 1.鼠标控制——MouseInc ​ MouseInc是一款创新的鼠标控制软件&#xff0c;可以让用户通过手势、声音或眼睛来控制鼠标的移动和点击。MouseInc利用了人工智能和计算机视觉的技术&#…

景联文科技语音数据标注:AUTO-AVSR模型和数据助力视听语音识别

ASR、VSR和AV-ASR的性能提高很大程度上归功于更大的模型和训练数据集的使用。 更大的模型具有更多的参数和更强大的表示能力&#xff0c;能够捕获到更多的语言特征和上下文信息&#xff0c;从而提高识别准确性&#xff1b;更大的训练集也能带来更好的性能&#xff0c;更多的数据…

九章云极DataCanvas多模态大模型平台实践与思考

导读&#xff1a;本文将分享九章云极DataCanvas在多模态大模型平台方面的一些思考和实践。 今天的介绍会围绕下面四点展开&#xff1a; 多模态大模型的历史发展 九章云极DataCanvas的多模态大模型平台 九章云极DataCanvas多模态大模型的实践 对未来的思考与展望 ▌多模态…

单片机点亮led管(01)

如何开始学习单片机 1&#xff1a;实践第一 2&#xff1a;补充必要的理论知识&#xff0c;缺什么补什么 3&#xff1a;做工程积累经验&#xff08;可以在网络上收集题目&#xff0c;也可以有自己的想法大胆的实验&#xff09; 单片机是什么&#xff1f; 单片机&#xff08…

活动回顾 | MatrixOne 在 SaaS 企服领域的应用解读

9月3日&#xff0c;矩阵起源产品总监邓楠于 QCon 北京站首次分享了 MatrixOne 在 SaaS 企服领域的应用&#xff0c;本篇文章将对该次分享进行回顾。 Part 1 MatrixOne 是什么&#xff1f; MatrixOne 是一款面向未来的超融合异构云原生数据库管理系统。通过全新从零自研的统一…

Pika v3.5.1发布!

导读Pika 社区很高兴宣布&#xff0c;我们今天发布已经过我们生产环境验证 v3.5.1 版本&#xff0c;https://github.com/OpenAtomFoundation/pika/releases/tag/v3.5.1 。 该版本不仅做了很多优化工作&#xff0c;还引入了多项新功能。这些新功能包括 动态关闭 WAL、Replicati…

如何打造品牌爆文,小红书爆文封面教程

在小红书平台&#xff0c;爆文其实是核心竞争力&#xff0c;你的流量取决于你生产爆文的稳定程度。而对于一篇文章而言&#xff0c;最重要的即是封面。今天来分享下如何打造品牌爆文&#xff0c;小红书爆文封面教程&#xff01; 1.了解用户人群特点 深入了解目标用户人群的特点…

数字化 | 智能电子日历

想要一款随时随地都能掌握日期的电子日历吗&#xff1f; WiFi通信&#xff0c;实时更新&#xff0c;超低功耗&#xff0c;可充电&#xff0c;超长续航&#xff0c;电子纸&#xff0c;黑白红三色显示的电子日历&#xff0c;就是你的最佳选择&#xff01; 无论是在办公室、家中或…

更新 | 持续开源 迅为RK3568驱动指南第十一篇-pinctrl子系统

《iTOP-RK3568开发板驱动开发指南》更新&#xff0c;本次更新内容对应的是驱动&#xff08;第十一期_pinctrl子系统-全新升级&#xff09;视频&#xff0c;后续资料会不断更新&#xff0c;不断完善&#xff0c;帮助用户快速入门&#xff0c;大大提升研发速度。 文档教程更新至第…

模拟退火算法求解TSP问题(python)

模拟退火算法求解TSP的步骤参考书籍《Matlab智能算法30个案例分析》。 问题描述 TSP问题描述在该书籍的第4章 算法流程 部分实现代码片段 坐标轴转换成两点之间直线距离长度的代码 coordinates np.array([(16.47, 96.10),(16.47, 94.44),(20.09, 92.54),(22.39, 93.37),(2…