• 77. 组合、216. 组合总和 III、17. 电话号码的字母组合


    77. 组合

    题目描述:

    给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

    你可以按 任何顺序 返回答案。

    解答:

    如果按照暴力来想,直接for循环,但是这样对于k较小的是可以,但是如果k为100,总不能写100层的for循环吧,那也太离谱了。

    所以还是应该想想其他办法。

    组合问题——回溯!

    在做二叉树的时候222. 完全二叉树的节点个数、110. 平衡二叉树、257.二叉树的所有路径_清榎的博客-CSDN博客t

    提到过回溯。回溯和递归是一体的,属于是那种你中有我,我中有你的关系。 

    回溯。回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

    所以其实回溯法并不高效,但是遇到一些问题,没办法,除了暴力就是回溯了,最多是 回溯+剪枝。

    先插个眼,回溯法,一般可以解决如下几种问题:

    • 组合问题:N个数里面按一定规则找出k个数的集合
    • 切割问题:一个字符串按一定规则有几种切割方式
    • 子集问题:一个N个数的集合里有多少符合条件的子集
    • 排列问题:N个数按一定规则全排列,有几种排列方式
    • 棋盘问题:N皇后,解数独等等

    回溯可以把问题抽象成一棵树,树的宽度和深度都有限,其中深度也就是递归的层数,宽度就是问题的大小

    表现在编程时也就是深度使用递归,宽度使用for循环,因此 回溯的一般写法为:

    1. void backtracking(参数) {
    2. if (终止条件) {
    3. 存放结果;
    4. return;
    5. }
    6. for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    7. 处理节点;
    8. backtracking(路径,选择列表); // 递归
    9. 回溯,撤销处理结果
    10. }
    11. }

    接下来就正式解题了,

    为了思路清晰仿照递归三部曲也整一个回溯三部曲:

    (1)参数及返回值:

            n,k肯定需要当作参数,还有下次从哪开始找也需要记录

            返回值可以不需要,直接开全局变量记录即可 。

    (2)终止条件:

            树搜索到叶子就可以终止了,叶子是什么情况呢?

            满足k个数——即为path中数量为k

    (3)内部处理逻辑:

            先放入当前访问的节点,再递归,递归完成后进行回溯。

    代码实现:

    1. class Solution {
    2. public:
    3. vectorint>>result;
    4. vector<int>path;
    5. void backTrace(int n, int k, int start){
    6. if (path.size() == k){
    7. result.push_back(path);
    8. return;
    9. }
    10. for (int i = start; i <= n; i++){
    11. path.push_back(i);
    12. backTrace(n, k, i+1);
    13. path.pop_back();
    14. }
    15. }
    16. vectorint>> combine(int n, int k) {
    17. backTrace(n, k, 1);
    18. return result;
    19. }
    20. };

    剪枝优化:

    前面提到回溯往往可以进行剪枝,这道题也可以。

    比如(4,4)这种情况。

    后面的数根本没四个了,还判断啥,没必要了,直接剪枝。

    所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置

    如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

    修改for循环条件:n - (k - path.size()) + 1

    216. 组合总和 III

    题目描述:

    找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

    只使用数字1到9
    每个数字 最多使用一次 
    返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

    解答:

    这道题和上道题十分相似,不一样的是把树的宽度给隐藏了

    很清楚,树的深度为k,那宽度应该取多少?

    数字1-9,所以宽度为9

    这样一来就简单了,每次取一个值放入path中,一直取到path中个数等于k为止。

    再判断是否等于n,等于就放入result里。

    然后返回,回溯 ,继续往下寻找即可。

    回溯三部曲:

    (1)参数及返回值:

            n,k肯定需要当作参数,还有下次从哪开始找也需要记录

            返回值可以不需要,直接开全局变量记录即可 。

    (2)终止条件:

            树搜索到叶子就可以终止了,叶子是什么情况呢?

            满足k个数——即为path中数量为k

    (3)内部处理逻辑:

            先放入当前访问的节点,再递归,递归完成后进行回溯。

    代码实现:

    1. class Solution {
    2. public:
    3. vectorint>>result;
    4. vector<int>path;
    5. void backTrace(int n, int k, int start){
    6. if (path.size() == k){
    7. int sum = 0;
    8. for (int i = 0; i < k; i++){
    9. sum += path[i];
    10. }
    11. if (sum == n)
    12. result.push_back(path);
    13. return;
    14. }
    15. for (int i = start; i <= 9; i++){
    16. path.push_back(i);
    17. backTrace(n, k, i + 1);
    18. path.pop_back();
    19. }
    20. }
    21. vectorint>> combinationSum3(int k, int n) {
    22. backTrace(n, k, 1);
    23. return result;
    24. }
    25. };

    剪枝优化:

    比较容易想到的是,如果我们当前节点的sum和已经大于等于n,那就没有继续往下寻找的必要了,直接剪枝。

    这种需要将sum作为参数进行传递,同时回溯时也需要相应对sum进行修改。

    代码实现:

    1. class Solution {
    2. public:
    3. vectorint>>result;
    4. vector<int>path;
    5. void backTrace(int n, int k, int start, int sum){
    6. //剪枝操作
    7. if (sum > n)
    8. return;
    9. if (path.size() == k){
    10. if (sum == n)
    11. result.push_back(path);
    12. return;
    13. }
    14. for (int i = start; i <= 9; i++){
    15. path.push_back(i);
    16. sum += i;
    17. backTrace(n, k, i + 1, sum);
    18. sum -= i;
    19. path.pop_back();
    20. }
    21. }
    22. vectorint>> combinationSum3(int k, int n) {
    23. backTrace(n, k, 1, 0);
    24. return result;
    25. }
    26. };

    当然也可以和上一题一样,对for循环进行剪枝,条件改为

    9 - (k - path.size()) + 1

     即可。

    17. 电话号码的字母组合

    题目描述:

    给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

    给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

    解答:

    这个问题本质上和前两题还是类似。

    需要考虑三个问题:

    数字和字母如何映射?

    树的宽度?

    树的深度?

    字母和数字的映射可以采用map或者数组,如下所示:

    1. const string letterMap[10] = {
    2. "", // 0
    3. "", // 1
    4. "abc", // 2
    5. "def", // 3
    6. "ghi", // 4
    7. "jkl", // 5
    8. "mno", // 6
    9. "pqrs", // 7
    10. "tuv", // 8
    11. "wxyz", // 9
    12. };

     至于树的宽度深度,我们拿到一串数字,数字的长度其实就是树的深度;每一个数字都会对应三个或者四个字母,这便是树的宽度。如下图所示

    17. 电话号码的字母组合

    考虑清楚这三个问题就可以开始编写代码了。

    回溯三要素:

    (1)参数和返回值:

    首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量依然定义为全局。

    再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index,标识从哪个数字开始。

    (2)终止条件:长度等于输入的数字长度,记录进结果中。

    (3)内部处理逻辑:拿到数字先获取到其对应的字母,然后进入循环,递归。 

    1. int digit = digits[index] - '0'; // 将index指向的数字转为int
    2. string letters = letterMap[digit]; // 取数字对应的字符集
    3. for (int i = 0; i < letters.size(); i++) {
    4. s.push_back(letters[i]); // 处理
    5. backTrace(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
    6. s.pop_back(); // 回溯
    7. }

    代码实现:

    1. class Solution {
    2. public:
    3. const string letterMap[10] = {
    4. "", // 0
    5. "", // 1
    6. "abc", // 2
    7. "def", // 3
    8. "ghi", // 4
    9. "jkl", // 5
    10. "mno", // 6
    11. "pqrs", // 7
    12. "tuv", // 8
    13. "wxyz", // 9
    14. };
    15. vector result;
    16. string s;
    17. void backTrace(const string& digits, int index){
    18. if (index == digits.size()) {
    19. result.push_back(s);
    20. return;
    21. }
    22. int digit = digits[index] - '0'; // 将index指向的数字转为int
    23. string letters = letterMap[digit]; // 取数字对应的字符集
    24. for (int i = 0; i < letters.size(); i++) {
    25. s.push_back(letters[i]); // 处理
    26. backTrace(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
    27. s.pop_back(); // 回溯
    28. }
    29. }
    30. vector letterCombinations(string digits) {
    31. if (digits.size() == 0) {
    32. return result;
    33. }
    34. backTrace(digits, 0);
    35. return result;
    36. }
    37. };

     

     

     

  • 相关阅读:
    创新案例|Amazon.com 2023 年营销策略:电子商务零售巨头商业案例研究
    MQ高级特性(消息的可靠性)
    【电驱动】永磁同步电机工作原理及分类
    Redis主从复制
    你需要知道的 TCP 四次挥手
    WPF——给button添加背景图片
    MySQL——增删改查进阶
    C#操作INI文件
    python使用mongoDB
    通过数(判断一个数是否在集合M中)
  • 原文地址:https://blog.csdn.net/weixin_41997940/article/details/126366870