《代码随想录》专题:回溯算法1


  • 母题清单
    • 77. 组合(使用回溯算法,考虑使用剪枝操作)
      • 216. 组合总和 III(子题,有两个剪枝操作)
      • 17.电话号码的字母组合(子题,把数字换成了字母,原理是一样的,但这道题无法进行剪枝操作,这道题目可以讲回溯算法理解更加透彻)

1、组合问题

  • 题目链接:77. 组合

  • 题解

    • 把组合问题抽象为如下树形结构
      《代码随想录》专题:回溯算法1_第1张图片
      图中每次搜索到了叶子节点,我们就找到了一个结果。相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。
    • 最难理解的是单层搜索的过程
      回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
      《代码随想录》专题:回溯算法1_第2张图片
      如此我们才遍历完图中的这棵树。for循环每次从startIndex开始遍历,然后用path保存取到的节点i。代码如下:
      for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
          path.push_back(i); // 处理节点
          backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
          path.pop_back(); // 回溯,撤销处理的节点
      }
      
      可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。
  • 代码

    class Solution {
    private:
        vector<vector<int>> result; // 存放符合条件结果的集合
        vector<int> path; // 用来存放符合条件结果
        void backtracking(int n, int k, int startIndex) {
            if (path.size() == k) {
                result.push_back(path);
                return;
            }
            for (int i = startIndex; i <= n; i++) {
                path.push_back(i); // 处理节点
                backtracking(n, k, i + 1); // 递归
                path.pop_back(); // 回溯,撤销处理的节点
            }
        }
    public:
        vector<vector<int>> combine(int n, int k) {
            backtracking(n, k, 1);
            return result;
        }
    };
    
    • 时间复杂度: O(n * 2^n)
    • 空间复杂度: O(n)
  • 给出回溯算法模板

    void backtracking(参数) {
        if (终止条件) {
            存放结果;
            return;
        }
    
        for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
            处理节点;
            backtracking(路径,选择列表); // 递归
            回溯,撤销处理结果
        }
    }
    
  • 这道题目还可以在进行优化,也就是剪枝操作。举一个例子,当n = 4k = 4时,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。
    《代码随想录》专题:回溯算法1_第3张图片所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。接下来看一下优化过程如下:

    • 已经选择的元素个数:path.size();
    • 所需要的元素个数为: k - path.size();
    • 列表中可提供的剩余元素个数:n-i+1; 为什么要+1?举个例子,n=4i=2,此时列表剩余的元素为[2,3,4],也就是4-2+1 = 3
    • 我们需要满足 列表中可提供的剩余元素个数>=所需要的元素个数 ,也就是 n-i+1 >= k - path.size(),最后化简得到 i <= n - (k - path.size()) + 1

    最后,优化后的for循环是:

    for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
    

    优化后整体代码如下:

    class Solution {
    private:
        vector<vector<int>> result;
        vector<int> path;
        void backtracking(int n, int k, int startIndex) {
            if (path.size() == k) {
                result.push_back(path);
                return;
            }
            for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
                path.push_back(i); // 处理节点
                backtracking(n, k, i + 1);
                path.pop_back(); // 回溯,撤销处理的节点
            }
        }
    public:
    
        vector<vector<int>> combine(int n, int k) {
            backtracking(n, k, 1);
            return result;
        }
    };
    

你可能感兴趣的:(《代码随想录》,算法)