代码随想录算法训练营三期 day 27 - 回溯 (3) (补)

news2024/10/6 8:29:29

39. 组合总和

题目链接:39. 组合总和
原文链接:39. 组合总和
视频链接:39. 组合总和
本题和 77.组合 ,216.组合总和III 的区别是:本题没有数量要求,可以无限重复,但是有总和的限制。
树形结构:
请添加图片描述

回溯三部曲:
确定递归函数参数及返回值:
这里依然是定义两个全局变量,二维数组 res 存放结果集,数组 path 存放符合条件的结果。
首先是题目中给出的参数,集合 candidates, 和目标值 target。
sum 用于统计单一结果 path 里的总和。
本题还需要 startIndex 来控制 for 循环的起始位置。

对于组合问题,什么时候需要 startIndex 呢?
如果是一个集合来求组合的话,就需要 startIndex,例如:77. 组合,216. 组合总和III。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用 startIndex,例如:17. 电话号码的字母组合。

vector<int> path;
vector<vector<int>> res;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)

递归终止条件:
从叶子节点可以清晰看到,终止只有两种情况,sum 大于 target 和 sum 等于 target。
sum 等于 target 的时候,需要收集结果,代码如下:

if (sum > target) {
    return;
}
if (sum == target) {
    res.push_back(path);
    return;
}

单层搜索的逻辑
单层 for 循环依然是从 startIndex 开始,搜索 candidates 集合。
注意本题和 77. 组合、216. 组合总和III 的一个区别是:本题元素为可重复选取的,体现在递归中。

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

完整代码:

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            res.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);
            path.pop_back();
            sum -= candidates[i];
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates, target, 0, 0);
        return res;
    }
};

剪枝:
对于 sum 已经大于 target 的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断 sum > target 的话就返回。
其实如果已经知道下一层的 sum 会大于 target,就没有必要进入下一层递归了。
那么可以在 for 循环的搜索范围上做文章了。
总集合排序 之后,如果下一层的 sum(就是本层的 sum + candidates[i])已经大于 target,就可以结束本轮 for 循环的遍历。
在求和问题中,排序之后加剪枝是常见的套路!

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            res.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return res;
    }
};

40. 组合总和II

题目链接:40. 组合总和II
原文链接:40. 组合总和II
视频链接:40. 组合总和II
这道题目和 39. 组合总和 如下区别:
(1) 本题 candidates 中的每个数字在每个组合中只能使用一次。
(2) 本题数组 candidates 的元素是有重复的,而 39.组合总和 是无重复元素的数组 candidates。
本题的难点在于区别 2 中:集合(数组 candidates)有重复元素,但还不能有重复的组合。
所谓去重,其实就是使用过的元素不能重复选取。
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
元素在同一个组合内是可以重复的,但两个组合不能相同。
所以要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
回溯三部曲:
递归函数参数:
与 39. 组合总和 套路相同,此时还需要加一个 bool 型数组 used,用来记录同一树枝上的元素是否使用过。这个集合去重的重任就是 used 来完成的。

    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {

递归终止条件
与 39. 组合总和 相同,终止条件为 sum > target 和 sum == target。

if (sum > target) { // 这个条件其实可以省略
    return;
}
if (sum == target) {
    res.push_back(path);
    return;
}

单层搜索的逻辑
这里与 39. 组合总和 最大的不同就是要去重了。
前面我们提到:要去重的是 “同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果 candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了 candidates[i - 1],也就是说同一树层使用过 candidates[i - 1]。
此时 for 循环里就应该做 continue 的操作。
请添加图片描述
注意 sum + candidates[i] <= target 为剪枝操作(注意要对数组进行排序)

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();
}

完整代码:

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            res.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            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);
            used[i] = false;
            path.pop_back();
            sum -= candidates[i];
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return res;
    }
};

131. 分割回文串

题目链接:131. 分割回文串
原文链接:131. 分割回文串
视频链接:131. 分割回文串
切割问题类似组合问题。
例如对于字符串 abcdef:
组合问题:选取一个 a 之后,在 bcdef 中再去选取第二个,选取 b 之后在 cdef 中再选取第三个…。
切割问题:切割一个 a 之后,在 bcdef 中再去切割第二段,切割 b 之后在 cdef 中再切割第三段…。
树形结构:
请添加图片描述
确定回溯函数参数与返回值
数组 path 存放切割后回文的子串,二维数组 res 存放结果集。
本题递归函数参数还需要 startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。

vector<string> path; 
vector<vector<string>> res;
void backtracking (const string& s, int startIndex) {

递归函数终止条件
请添加图片描述
从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。
在处理组合问题的时候,递归参数需要传入 startIndex,表示下一轮递归遍历的起始位置,这个 startIndex 就是切割线
所以终止条件代码如下:

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

单层递归逻辑
在 for (int i = startIndex; i < s.size(); i++) 循环中,我们 定义了起始位置 startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入在 vector<string> path 中,path 用来记录切割过的回文子串。
代码如下:

        for (int i = startIndex; i < s.size(); i++) {
            if (dp[startIndex][i]) {
                path.push_back(s.substr(startIndex, i - startIndex + 1));
            } else {
                continue;
            }
            backtracking(s, i + 1);
            path.pop_back();
        }

注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为 i + 1。
判断回文子串(动态规划):
(1) 确定 dp 数组及其下标的含义:
布尔类型的 dp[i][j] :表示区间范围 [i, j] (注意是左闭右闭)的子串是否是回文子串,如果是 dp[i][j] 为 true,否则为 false。
(2) 确定递推公式:
在确定递推公式时,就要分析如下几种情况。
整体上是两种,就是 s[i] 与 s[j] 相等,s[i] 与 s[j] 不相等这两种。
1️⃣ 当 s[i] 与 s[j] 不相等,dp[i][j] 一定是 false。
2️⃣ 当 s[i] 与 s[j] 相等时,这就复杂一些了,有如下三种情况:
❶ 下标 i 与 j 相同,同一个字符例如 a,当然是回文子串;
❷ 下标 i 与 j 相差为 1,例如 aa,也是回文子串;
❸ 下标 i 与 j 相差大于 1 的时候,例如 cabac,此时 s[i] 与 s[j] 已经相同了,我们看 i 到 j 区间是不是回文子串就看 aba 是不是回文就可以了,那么 aba 的区间就是 i + 1 与 j - 1 区间,这个区间是不是回文就看 dp[i + 1][j - 1] 是否为 true。
以上三种情况分析完了,那么递归公式如下:

if (s[i] == s[j]) {
    if (j - i <= 1) {
        dp[i][j] = true;
    } else {
        if (dp[i + 1][j - 1]) {
            dp[i][j] = true;
    	}
    }
}

(3) 初始化:
dp[i][j] 初始化为 false。
(4) 确定遍历顺序:
从下到上,从左到右。
请添加图片描述
(5) 举例推导 dp 数组:
请添加图片描述

    void getDp(const string& s) {
        dp.resize(s.size(), vector<bool>(s.size(), false));
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) {
                        dp[i][j] = true;
                    } else {
                        if (dp[i + 1][j - 1]) {
                            dp[i][j] = true;
                        }
                    }
                }
            }
        }
    }

完整代码如下:

class Solution {
private:
    vector<string> path;
    vector<vector<string>> res;
    vector<vector<bool>> dp;
    void backtracking(const string& s, int startIndex) {
        if (startIndex >= s.size()) {
            res.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {
            if (dp[startIndex][i]) {
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
            } else {
                continue;
            }
            backtracking(s, i + 1);
            path.pop_back();
        }
    }
    void getDp(const string& s) {
        dp.resize(s.size(), vector<bool>(s.size(), false));
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) {
                        dp[i][j] = true;
                    } else {
                        if (dp[i + 1][j - 1] == true) {
                            dp[i][j] = true;
                        }
                    }
                }
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        getDp(s);
        backtracking(s, 0);
        return res;
    }
};

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

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

相关文章

【axios】axios的基础知识和使用

一、基础知识概念Axios 是专注于网络数据请求的库,只负责发请求、拿数据&#xff0c;不能操作DOM元素。相比于原生的 XMLHttpRequest 对象&#xff0c;axios 简单易用。相比于 jQuery&#xff0c;axios 更加轻量化&#xff0c;不能操作DOM元素&#xff0c;只专注于网络数据请求…

cubeIDE开发, stm32人工智能开发应用实践(Cube.AI).篇二

一、事有蹊跷 接篇一&#xff0c;前面提到在使用cube.AI生成的c语言神经网络模型API调用时&#xff0c;输入数据数量是24&#xff0c;输出数据数量是4&#xff0c;但上文设想采集了三轴加速度传感器的x/y/z三个各数据&#xff0c;按Jogging(慢跑),Walking(走了)两种态势采集了两…

Java链表OJ题

目录1. 删除链表中等于给定值val的所有结点2. 逆置单链表3. 链表的中间结点4. 链表中倒数第k个结点5. 将两个有序链表合并为一个新的有序链表6. 以给定值x为基准将链表分割成两部分7. 判断是否为回文链表8. 两个链表的第一个公共结点9. 判断链表中是否有环10. 链表开始入环的第…

【Linux】目录权限和默认权限

上期介绍了Linux的文件权限&#xff0c;这期我们仔细来说说Linux环境下目录权限和默认权限一、目录权限1.1 进入目录所需的权限我们在进入目录时需要什么样的权限呢&#xff1f;是r、w还是x呢&#xff1f;下面我们一起来验证一下&#xff1a;&#x1f4cb;如下我门拥有全部目录…

Day11 AOP介绍

1 前言AOP&#xff0c;Aspect Oriented Programming&#xff0c;面向切面编程&#xff0c;是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象&#xff0c;一个对象包括静态的属性信息&#xff0c;包括动态的方法信息等。而AOP是横向的对不同事物的抽象&#xff0c;属性与属…

【Python从入门到精通】第一阶段

文章目录前言python的起源打印hello world注释变量变量基本概念类型类型转换运算符字符串拓展字符串的三种定义方法字符串拼接字符串格式化数据输入input比较布尔类型和比较运算符if判断if elseif elif else嵌套循环while循环while循环嵌套for循环range()的使用函数的使用函数的…

3小时精通opencv(五) 利用TrackBar进行颜色检测

3小时精通opencv(五) 利用TrackBar进行颜色检测 参考视频资源:3h精通Opencv-Python 本章内容介绍如何利用TrackBar调节色域, 手动提取到我们需要的颜色 文章目录3小时精通opencv(五) 利用TrackBar进行颜色检测创建Trackbar色彩检测创建Trackbar 在opencv中使用createTrackbar函…

C语言:数组

往期文章 C语言&#xff1a;初识C语言C语言&#xff1a;分支语句和循环语句C语言&#xff1a;函数 目录往期文章前言1. 一维数组的创建和初始化1.1 数组的创建1.2 数组的初始化2. 一维数组的使用3. 一维数组在内存中的存储4. 二维数组的创建和初始化4.1 二维数组的创建4.2 二维…

大数据技术架构(组件)7——Hive:Filter PushDown Cases And Outer Join Behavior

1.2、Filter PushDown Cases And Outer Join Behavior前提:关闭优化器set hive.auto.convertjoinfalse; set hive.cbo.enablefalse;Inner Join:1、Join On中的谓词: 左表下推、右表下推2、Where谓词:左表下推、右表下推-- 第一种情况: join on 谓词 selectt1.user_id,t2.user_i…

C++函数定义和调用介绍

C函数定义和调用介绍 函数的意义&#xff1a;利用率高&#xff0c;可读性强&#xff0c;利于移植。 一个C程序中主函数有且只有一个&#xff0c;是程序的入口&#xff0c;而函数&#xff08;或称子函数&#xff09;可以有很多。 每个 C 程序都至少有一个函数&#xff0c;即主…

2021 XV6 8:locks

实验有两个任务&#xff0c;都是为了减少锁的竞争从而提高运行效率。Memory allocator一开始我们是有个双向链表用来存储空闲的内存块&#xff0c;如果很多个进程要竞争这一个链表&#xff0c;就会把效率降低很多。所以我们把链表拆成每个CPU一个&#xff0c;在申请内存的时候就…

栈和队列的应用

一、栈在括号匹配中的应用 数据结构之栈_迷茫中的小伙的博客-CSDN博客_数据结构之栈栈括号和队列的应用 二、栈在表达式求值中的应用 中缀转 ->后缀 &#xff1a; 左右先 (左边能先算,先算左边,因为这样可以保证确定性,即计算机运算的方式) 后缀转->中缀 &#xff1a…

王者荣耀入门技能树-解答

前言 前段时间写了一篇关于王者荣耀入门技能树的习题&#xff0c;今天来给大家解答一下。 职业 以下哪个不属于王者荣耀中的职业&#xff1a; 射手法师辅助亚瑟 这道题选&#xff1a;亚瑟 王者荣耀中有6大职业分类&#xff0c;分别是&#xff1a;坦克、战士、刺客、法师、…

如何好好说话-第12章 理清楚问题就是答案

生活中该不该积极主动与别人展开社交活动&#xff1f;有些时候社交活动并不开心&#xff0c;仅仅只是无聊的闲才。但他确实能拉拢人际关系&#xff0c;帮我们获得近身套路。而且有一种观点认为不善于社交的人是不成功的。注意以上说的这些都是偏见。当我们站在一个更高的维度认…

Jetpack架构组件库:Hilt

Hilt Hilt 是基于 Dagger2 的依赖注入框架&#xff0c;Google团队将其专门为Android开发打造了一种纯注解的使用方式&#xff0c;相比 Dagger2 而言使用起来更加简单。 依赖注入框架的主要作用就是控制反转&#xff08;IOC, Inversion of Control&#xff09;, 那么什么是控制…

表格相关的一些标签

<!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>表格相关的标一些签</title> </head> <body> <!-- 需求 1&#xff1a;做一个四行&#xff0c;三…

Golang进阶

"白昼会边长&#xff0c;照亮心脏,让万物生长。"一、Golang进阶我们对golang的语法进行了一定的了解后&#xff0c;也算是入门了。本节的进阶篇围绕三个方向展开,Goroutine 、 Channel 、Sync。如何理解并行与并发&#xff1f;并行是指“并排行走”或“同时实行或实施…

用数组实现链表、栈和队列

目录前言一、用数组实现链表1.1 单链表1.2 双链表二、用数组实现栈三、用数组实现队列前言 众所周知&#xff0c;链表可以用结构体和指针来实现&#xff0c;而栈和队列可以直接调用STL&#xff0c;那为什么还要费尽心思用数组来实现这三种数据结构呢&#xff1f; 首先&#x…

好的质量+数量 = 健康的创作者生态

缘起 CSDN 每天都有近万名创作者发表各种内容&#xff0c; 其中博客就有一万篇左右。 这个数量是非常可喜的&#xff0c;这也是 CSDN 的产品、研发运营小伙伴、和各位博主持续工作的结果。 衡量一个 IT 内容平台&#xff0c;除了数量之外&#xff0c;还有另外一些因素&#xf…

Linux——动态库

目录 制作并发布动态库 使用动态库 使用动态库程序运行时的错误 制作并发布动态库 静态库的代码在链接的时候会被拷贝进对应的可执行程序内部&#xff0c;动态库则不需要拷贝。 动态库在形成目标文件时&#xff0c;需要加一个选项 -fPIC&#xff1a;形成一个与位置无关的二…