代码随想录Day 23|回溯Part02,39.组合总和、40.组合总和Ⅱ、131.分割回文串

news2024/9/20 20:49:28

提示:DDU,供自己复习使用。欢迎大家前来讨论~

文章目录

  • 第七章 回溯算法part03
  • 一、题目
    • 题目一: 39. 组合总和
      • 解题思路:
      • 回溯三部曲
      • 剪枝优化
      • 小结:
    • 题目二:40.组合总和Ⅱ
      • 解题思路:
      • 回溯三部曲
    • 题目三: 131.分割回文串
      • 解题思路
      • 回溯三部曲
      • 判断回文子串
      • 优化
  • 总结


第七章 回溯算法part03

开始补一下

一、题目

题目一: 39. 组合总和

39. 组合总和

解题思路:

本题是可以重复被选取,不会出现0,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。

直接的想法就是两层for循环,依次输出两个元素。

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

39.组合总和

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

回溯三部曲

排列和组合是不一样的。

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

如果是一个集合来求组合的话,就需要startIndex,例如77.组合,216.组合总和III 。

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

  • 递归函数参数
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)
  • 递归终止条件

在如下树形结构中:

39.组合总和

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

if (sum > target) {
    return;
}
if (sum == target) {
    result.push_back(path);
    return;
}
  • 单层搜索的逻辑

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

for (int i = startIndex; i < candidates.size(); i++) {
    sum += candidates[i];
    path.push_back(candidates[i]);
    backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
    sum -= candidates[i];   // 回溯
    path.pop_back();        // 回溯
}

完整代码:

// 版本一
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            result.push_back(path);
            return;
        }

        for (int i = startIndex; i < candidates.size(); i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

剪枝优化

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

39.组合总和1

for循环剪枝代码如下:

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)

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

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }

        // 如果 sum + candidates[i] > target 就终止遍历
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();

        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end()); // 需要排序
        backtracking(candidates, target, 0, 0);
        return result;
    }
};
  • 时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
  • 空间复杂度: O(target)

小结:

  • 组合没有数量要求
  • 元素可以无限重复取

在求和问题中,排序之后加剪枝是常见的套路!

题目二:40.组合总和Ⅱ

40. 组合总和 II

解题思路:

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

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

下面有used数组的使用:

40.组合总和II

回溯三部曲

  • 确定递归函数参数

依然需要一维数组path来存放符合条件的结果,二维数组result来存放结果集。

此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。这个集合去重的重任就是used来完成的。

vector<vector<int>> result; // 存放组合集合
vector<int> path;           // 符合条件的组合
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
  • 确定终止条件
if (sum > target) { // 这个条件其实可以省略
    return;
}
if (sum == target) {
    result.push_back(path);
    return;
}
  • 单层搜索过程

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

40.组合总和II1

在图中将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,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示:

img

注意sum + candidates[i] <= target为剪枝操作

回溯三部曲分析完了,整体C++代码如下:

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        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();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        path.clear();
        result.clear();
        // 首先把给candidates排序,让其相同的元素都挨在一起。
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return result;
    }
};
  • 时间复杂度: O(n * 2^n)
  • 空间复杂度: O(n)

小结:

去重逻辑很关键,涉及到回溯。

题目三: 131.分割回文串

131. 分割回文串

解题思路

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

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

例如对于字符串abcdef:

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

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

131.分割回文串

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


回溯三部曲

  • 确定回溯函数参数

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

vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
  • 确定终止条件
131.分割回文串

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

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

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

void backtracking (const string& s, int startIndex) {
    // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
    if (startIndex >= s.size()) {
        result.push_back(path);
        return;
    }
}
  • 确定单层遍历逻辑

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

[startIndex, i] 就是要截取的子串。

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

for (int i = startIndex; i < s.size(); i++) {
    if (isPalindrome(s, startIndex, i)) { // 是回文子串
        // 获取[startIndex,i]在s中的子串
        string str = s.substr(startIndex, i - startIndex + 1);
        path.push_back(str);
    } else {                // 如果不是则直接跳过
        continue;
    }
    backtracking(s, i + 1); // 寻找i+1为起始位置的子串
    path.pop_back();        // 回溯过程,弹出本次已经添加的子串
}

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

判断回文子串

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

bool isPalindrome(const string& s, int start, int end) {
     for (int i = start, j = end; i < j; i++, j--) {
         if (s[i] != s[j]) {
             return false;
         }
     }
     return true;
 }

回溯算法模板:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

完整C++代码:

class Solution {
private:
    vector<vector<string>> result;
    vector<string> path; // 放已经回文的子串
    void backtracking (const string& s, int startIndex) {
        // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
        if (startIndex >= s.size()) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {
            if (isPalindrome(s, startIndex, i)) {   // 是回文子串
                // 获取[startIndex,i]在s中的子串
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
            } else {                                // 不是回文,跳过
                continue;
            }
            backtracking(s, i + 1); // 寻找i+1为起始位置的子串
            path.pop_back(); // 回溯过程,弹出本次已经添加的子串
        }
    }
    bool isPalindrome(const string& s, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            if (s[i] != s[j]) {
                return false;
            }
        }
        return true;
    }
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        backtracking(s, 0);
        return result;
    }
};
  • 时间复杂度: O(n * 2^n)
  • 空间复杂度: O(n^2)

优化

  1. 动态规划:动态规划是一种解决子问题重叠问题的有效方法。在判断回文子串的问题中,可以预先计算出一个字符串的所有子串是否为回文,并存储这些结果。这样,在后续的判断中,可以直接查询这些预先计算的结果,而不需要重新计算。
  2. 动态规划的具体实现:可以创建一个二维布尔数组dp[i][j],其中ij分别表示子串的起始和终止索引。如果s[i] == s[j]并且dp[i + 1][j - 1]为真(即s[i + 1]s[j - 1]的子串是回文),则dp[i][j]也为真。这样,对于任意子串,只需要检查一次,并将结果存储在dp数组中。
class Solution {
private:
    vector<vector<string>> result;
    vector<string> path; // 放已经回文的子串
    vector<vector<bool>> isPalindrome; // 放事先计算好的是否回文子串的结果
    void backtracking (const string& s, int startIndex) {
        // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
        if (startIndex >= s.size()) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {
            if (isPalindrome[startIndex][i]) {   // 是回文子串
                // 获取[startIndex,i]在s中的子串
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
            } else {                                // 不是回文,跳过
                continue;
            }
            backtracking(s, i + 1); // 寻找i+1为起始位置的子串
            path.pop_back(); // 回溯过程,弹出本次已经添加的子串
        }
    }
    void computePalindrome(const string& s) {
        // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 
        isPalindrome.resize(s.size(), vector<bool>(s.size(), false)); // 根据字符串s, 刷新布尔矩阵的大小
        for (int i = s.size() - 1; i >= 0; i--) { 
            // 需要倒序计算, 保证在i行时, i+1行已经计算好了
            for (int j = i; j < s.size(); j++) {
                if (j == i) {isPalindrome[i][j] = true;}
                else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);}
                else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);}
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        computePalindrome(s);
        backtracking(s, 0);
        return result;
    }
};

小结:

列出如下几个难点:

  • 切割问题可以抽象为组合问题
  • 如何模拟那些切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 如何判断回文

总结出来难究竟难在哪里也是一种需要锻炼的能力


总结

  • 回溯问题的进阶
  • 判断回文串,可以重复选取元素,使用used数组

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

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

相关文章

LabVIEW中CANopen Read SDO.vi 和 CANopen Read Write CAN Frame.vi区别

CANopen Read SDO.vi 和 CANopen Read Write CAN Frame.vi 都是 NI-Industrial Communications for CANopen 库中的示例 VI&#xff0c;用于与 CANopen 网络进行通信&#xff0c;但它们的功能和使用场景有所不同。以下是它们的主要区别&#xff1a; 1. 功能层次 CANopen Read W…

图像分割论文阅读:BCU-Net: Bridging ConvNeXt and U-Net for medical image segmentation

本文提出了一种集合ConvNeXt和U-Net优势的网络模型来分割医学图像。 当然&#xff0c;模型整体结构就是并列双分支&#xff0c;如果只是这些内容&#xff0c;不值得拿出来讲。 主要有意思的部分是其融合两分支的多标签召回模块&#xff08;multilabel recall loss module&…

Tutorial:Deep Learning for Remote Sensing Data

文章目录 0. Intro1. ADVANTAGES OF REMOTE SENSING METHODS2. THE GENERAL FRAMEWORK3. BASIC ALGORITHMS IN DEEP LEARNING3.1 CONVOLUTIONAL NEURAL NETWORKS3.1.1 CONVOLUTIONAL LAYER3.1.2 NONLINEARITY LAYER3.1.3 POOLING LAYER 3.2 AUTOENCODERS3.3 RESTRICTED BOLTZMA…

SEO古诗网,可做站群,可二开成泛——码山侠

数据量大&#xff0c;古诗&#xff0c;名句等一共有数十万数据&#xff0c;基本上所有的古诗词已经入库完。 模板SEO强大&#xff0c;做好了基本的优化配置&#xff0c;结合帝国强大的sinfo插件&#xff0c;百度推送插件&#xff0c;以及itag管理插件很容易形成词库。 帝国CM…

数据结构(邓俊辉)学习笔记】串 01——ADT

1. 定义 特点 我们讨论的主题是串&#xff0c;无论从抽象数据类型&#xff0c;还是从具体实现的角度来看&#xff0c;串&#xff0c;相当于此前所介绍的数据结构来说都更为简单。因此&#xff0c;会将更多的时间用于讨论串的相关算法&#xff0c;尤其是串匹配的算法。 在接下…

探寻孩子不会说话与自闭症的关联及成因

在孩子的成长过程中&#xff0c;语言的发展是一个至关重要的阶段。然而&#xff0c;有些孩子却迟迟不会说话&#xff0c;这让家长们忧心忡忡。而当孩子不会说话的同时还伴有一些异常行为时&#xff0c;自闭症的担忧便会涌上心头。那么&#xff0c;孩子不会说话且患有自闭症究竟…

最近再写一个仿微信的项目遇到的一些bug(一)

目录&#xff1a; bug &#xff08;一&#xff09;Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are requiredProperty报错解决方法 bug &#xff08;二&#xff09;Cannot invoke “javax.script.ScriptEngine.eval(String)“ because “engine“ is null报错原…

likeshop采集商品图片无法保存解决方案

封面图 一个修复单&#xff0c;客户的likeshop采集tb商品后&#xff0c;保存到商品库的时候 主图无法显示 报错&#xff1a; "/www/wwwroot/test.0ev.cn/server/public/uploads/l7pu2aqt/admin/images/d61d40dab9e6245f90b62ede72b51639.jpg" string(6226) "…

除毛大作战,选择你的清理工具——希喂、美的宠物空气净化器PK

随着气温的升高&#xff0c;又到了宠物的换毛季。猫咪在家里疯狂掉毛&#xff0c;而铲屎官也陷入清理难题。幸好&#xff0c;有宠物空气净化器可以帮助铲屎官减轻打扫负担。那么宠物空气净化器又该如何挑选呢&#xff1f;哪款宠物空气净化器效果更佳&#xff1f;我也很想知道答…

【JVM】剖析字符串与数组的底层实现(一)

剖析字符串与数组的底层实现 字符数组的存储方式 JVM有三种模型: 1.Oop模型:Java对象对应的C对象2.Klass模型:Java类在JVM对应的C对象3.handle模型 字符串常量池 即String Pool&#xff0c;但是JVM中对应的类是StringTable&#xff0c;底层实现是一个hashtable,如代码所示 …

老师怎样分班更便捷?

随着新学期的钟声敲响&#xff0c;老师们又迎来了一年中最繁忙的时刻。开学之初&#xff0c;除了要处理日常的教学事务&#xff0c;老师们还肩负着一项重要任务——给新生进行分班。 其实老师们完全可以不必那么劳累。在这个科技日新月异的时代&#xff0c;有许多工具可以帮助老…

计算机毕业设计选题推荐-高中素质评价档案系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

SOMEIP_ETS_061: Sending_two_SOMEIP_Messages_in_a_row

测试目的&#xff1a; 验证设备&#xff08;DUT&#xff09;能够处理在单个UDP数据包中发送的多个SOME/IP消息&#xff0c;并对所有这些SOME/IP消息给出正确的响应。 描述 本测试用例旨在检查DUT在接收到一个包含多个SOME/IP消息的UDP数据包时&#xff0c;是否能够对所有包含…

如何使用MQTT订阅摄像机/NVR/DVR的AI报警

H5S内置MQTT服务&#xff0c;并把设备报警默认推送到MQTT服务器上&#xff0c;进入 设置-》协议-》MQTT配置MQTT服务参数&#xff0c;配置后需要重启生效。 MQTT开启后&#xff0c;就可以使用第三方MQTT客户端订阅事件&#xff0c;以下以MQTTX( https://mqttx.app/ )为例。 链…

深入解析css-学习小结

绪论 盒模型 层叠 优先级 继承 层叠 层叠指规则冲突时&#xff0c;如何选择规则。规则冲突解决顺序&#xff1a; 样式表来源 用户代理样式 用户代理样式&#xff1a;浏览器默认样式 作者样式表&#xff1a;你自己写的css样式 作者样式表会覆盖用户代理样式&#xff0c;因…

宅家必备神器!远程控制软件,让你随时随地掌控一切

在数字化时代&#xff0c;远程控制软件已经成为我们日常生活和工作中不可或缺的工具。今天&#xff0c;我将分享五款我使用过的远程控制软件的使用感受&#xff0c;希望大家能够选择到一款适合自己的远控工具&#xff1a; 一、向日葵远程控制 直通车&#xff08;粘贴到浏览器…

基于xr-frame实现微信小程序的图片扫描识别AR功能(含源码)

前言 xr-frame是一套小程序官方提供的XR/3D应用解决方案&#xff0c;基于混合方案实现&#xff0c;性能逼近原生、效果好、易用、强扩展、渐进式、遵循小程序开发标准。xr-frame在基础库v2.32.0开始基本稳定&#xff0c;发布为正式版&#xff0c;但仍有一些功能还在开发&#…

NWM口罩佩戴检测算法,浅析口罩佩戴检测从源码到实际应用的全面指南

一、背景 随着新冠疫情的全球蔓延&#xff0c;佩戴口罩成为了预防病毒传播的重要措施。然而&#xff0c;随着疫情的持续&#xff0c;社会上仍存在不少未佩戴口罩的行为&#xff0c;这给公共健康带来了巨大的风险。在这样的背景下&#xff0c;基于计算机视觉的口罩检测算法应运…

ML307R_APP_DEMO_SDK TCP/UDP使用介绍

ML307R_APP_DEMO_SDK是在ML307R_OpenCPU_Standard_SDK标准代码基础上&#xff0c;新增了面向用户APP层的demo示例&#xff0c;与标准代码中examples的示例代码不同&#xff0c;app_demo实现了联网自动化&#xff0c;数据透传&#xff0c;各功能可独立自动运行&#xff0c;并对用…

【TB作品】TM1637芯片数码管,PIC16单片机驱动显示,Proteus仿真

文章目录 效果模块芯片介绍code 效果 只能是共阳数码管&#xff1a; 模块 芯片介绍 TM1637芯片是一种常用于LED数码管显示控制的驱动芯片&#xff0c;下面是各引脚的详细说明&#xff1a; DIO (Data Input/Output) - 管脚号: 17 功能: 串行数据输入/输出&#xff0c;DIO引脚…