第二十四天 | 回溯算法 part01

回溯算法理论基础

文字讲解:回溯算法理论基础

视频讲解:https://www.bilibili.com/video/BV1cy4y167mM

剪枝讲解:https://www.bilibili.com/video/BV1wi4y157er

定义:回溯法穿插在递推的过程中,本质上是一种暴力搜索算法,穷举所有可能。其解决的以下问题都可以抽象为树形结构

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

第77题. 组合

文字讲解:组合

视频讲解:https://www.bilibili.com/video/BV1ti4y1L7cv

剪枝操作:https://www.bilibili.com/video/BV1wi4y157er

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

思路:所有回溯算法的题目,都要先将问题抽象为树形结构,本题如下图:

<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195223940.png" alt="77.组合" style="zoom:50%;" />

回溯法三部曲:

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

     在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
    

<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195328976.png" alt="77.组合2" style="zoom:50%;" />

  • 回溯函数终止条件:

     path数组的大小达到k,说明找到了一个子集大小为k的组合,此时用result二维数组,把path保存起来,并终止本层递归。
    

<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195407907.png" alt="77.组合3" style="zoom:50%;" />

  • 单层搜索的过程:

     for循环用来横向遍历,递归的过程是纵向遍历。
    

<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195242899.png" alt="77.组合1" style="zoom:50%;" />

未剪枝操作如下:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }

    public void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            path.add(i);
            backtracking(n, k, i + 1);
            path.removeLast();
        }
    }
}

下图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。

<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20210130194335207-20230310134409532.png" alt="77.组合4" style="zoom:50%;" />

剪枝操作如下:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }

    public void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            path.add(i);
            backtracking(n, k, i + 1);
            path.removeLast();
        }
    }
}

优化过程如下:

  1. 已经选择的元素个数:path.size();
  2. 还需要的元素个数为: k - path.size();
  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容