代码随想录算法训练营第二十四天 | 回溯理论基础、77.组合

回溯理论基础

回溯时递归的副产品,只要有递归就会有回溯

回溯的效率

回溯的本质是穷举,穷举所有可能,然后选出想要的答案l如果想更高效一些,可以加一些剪枝操作

回溯法解决的问题

  • 组合问题:N 个数里按一定规划找出 k 个数的集合

  • 切割问题:一个字符串按一定规则有几种切割方式

  • 子集问题:一个 N 个数的集合里有多少符合条件的子集

  • 排列问题:N 个数按一定规则全排列,有几种排列方式

  • 棋盘问题:N 皇后,解数独等

组合是不强调元素顺序的,排列时强调元素顺序

例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合

组合无序,排列有序

理解回溯法

所有回溯法解决的问题都可以抽象为树形结构

回溯法解决在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度

递归有终止条件,所以必然是一棵高度有限的树(N 叉树)

回溯法模板

回溯三部曲:

  • 回溯函数模板返回值以及参数

    • 函数习惯命名 backtracking
    • 返回值一般为 void
    • 参数一般是先写逻辑,然后需要什么再填
  • 回溯函数终止条件

    • 存放结果并结束本层递归时
  • 回溯搜索的遍历过程

    • 回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成树的深度
      代码随想录算法训练营第二十四天 | 回溯理论基础、77.组合_第1张图片

    • 注意图中举例集合大小和孩子数量是相等的

    • 回溯函数遍历过程伪代码

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

整体模板如下:

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

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

77.组合

题目

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

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

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

提示:

  • 1 <= n <= 20

  • 1 <= k <= n

思路

将组合问题抽象为如下树形结构

代码随想录算法训练营第二十四天 | 回溯理论基础、77.组合_第2张图片

可以看出这棵树,一开始集合是 1,2,3,4,从左向右取数,取过的数不再重复取

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围

图中 n 为树的宽度,k 为树的深度

每次搜索到了叶子节点,就找到了一个结果

回溯法三部曲:

  • 递归函数的返回值以及参数

    • 定义两个全局变量,一个存放符合条件的单一结果,一个存放符合条件结果的集合(题目要求)
    • n 和 k
    • int 型 startIndex,记录本层递归中,集合从哪里开始遍历,防止出现重复的组合。如下图红线,在集合 [1,2,3,4] 取 1 后,下一层递归就要在 [2,3,4] 中取数,如何知道从 [2,3,4] 取数靠的就是 startIndex
      • 代码随想录算法训练营第二十四天 | 回溯理论基础、77.组合_第3张图片
  • 回溯函数终止条件

    • path 数组的大小如果等于 k,说明找到了一个子集大小为 k 的组合,用 result 保存,并终止本层递归
  • 单层搜索过程

    • for 循环用来横向遍历,递归的过程是纵向遍历
      • 代码随想录算法训练营第二十四天 | 回溯理论基础、77.组合_第4张图片

      • for 每次从 startIndex 开始遍历,用 path 保存取到的节点 i

剪枝

遍历范围优化,举例 n = 4,k = 4,那么第一层 for 循环时,从元素 2 开始的遍历都没有意义了

代码随想录算法训练营第二十四天 | 回溯理论基础、77.组合_第5张图片

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

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

代码实现

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

你可能感兴趣的:(算法,算法)