【代码随想录】d30-回溯算法-part06-总结-python

1. 回溯算法理论

1.1 回溯算法简介

回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。

回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。

1.2回溯算法解决的问题

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

1.3 回溯算法的解题思路

将遍历过程抽象为一个树形结构

  • for循环横向遍历,递归纵向遍历,用递归控制for循环嵌套的数量,回溯不断调整结果集
  • startIndex来控制for循环的起始位置
  • 剪枝
    • for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了
    • 在for循环上做剪枝操作是回溯法剪枝的常见套路
  • 去重
    • 树层去重
    • 树枝去重

1.4回溯算法模板

针对需要使用回溯算法的问题,可以使用回溯三部曲来分析回溯算法

  • 回溯函数模板返回值以及参数
  • 回溯函数终止条件
  • 回溯搜索的遍历过程

回溯法的模板:

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

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

2. 回溯算法相关题目

2.1组合问题

2.1.1组合问题

文章链接:https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。
    示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
    示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

1. 分析
通常在做这样的题目时,会考虑使用暴力解法,需要几个数就用几层for循环,但是在k值不确定的情况下,无法确定具体要用几层for循环,这时候就需要使用递归来替代多层for循环,即深度方向的循环。

画出树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第1张图片
可以直观的看出其搜索的过程:for循环横向遍历,递归纵向遍历,回溯不断调整结果集
2. 剪枝
优化回溯算法只有剪枝一种方法
树形结构如图:
【代码随想录】d30-回溯算法-part06-总结-python_第2张图片
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。

在for循环上做剪枝操作是回溯法剪枝的常见套路!

2.1.2 组合总和

文章链接:https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。
    示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
    示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

1. 分析
本题相比2.1.1组合问题加了一个元素总和的限制
树形结构如图:
【代码随想录】d30-回溯算法-part06-总结-python_第3张图片
整体思路是一样的,只是需要多记录一个和的结果,用于和target做比较
2.剪枝
本题可以进行2个剪枝

  • 2.1.1组合问题相同的for循环取值范围的剪枝
  • 总和大于>target的剪枝
    • 已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉
      【代码随想录】d30-回溯算法-part06-总结-python_第4张图片

2.1.3 组合总和(二)

文章链接:https://programmercarl.com/0039.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C.html

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。
    示例 1:
  • 输入:candidates = [2,3,6,7], target = 7,
  • 所求解集为: [ [7], [2,2,3] ]
    示例 2:
  • 输入:candidates = [2,3,5], target = 8,
  • 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

1. 分析
本题和2.1.2组合总和问题相比,去掉了数字个数的限制,元素可以无限制重复被选取,保留了总和大小限制,所以间接的也是有个数的限制。
虽然没有限制每个元素只能取一次,但是还是需要startindex来控制for循环的起始位置,例如[2,3,5],如果不控制,那么当取3时,还可以取到2,那么就会有[3,3,2]、[3,2,3]这两个组合,这两个组合与[2,3,3]实际上是重复的组合
什么时候需要使用startindex?

  • 如果是一个集合来求组合的话,就需要startIndex
  • 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
    树形结构如下:
    【代码随想录】d30-回溯算法-part06-总结-python_第5张图片
    2.剪枝
    本题剪枝只会用到和大于目标值这一种剪枝方式
    优化后树形结构如下:
    【代码随想录】d30-回溯算法-part06-总结-python_第6张图片

2.1.4 组合总和(三)

文章链接:https://programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:

  • 输入: candidates = [10,1,2,7,6,1,5], target = 8,
  • 输出:
    [
    [1,1,6],
    [1,2,5],
    [1,7],
    [2,6]
    ]
    示例 2:
  • 输入: candidates = [2,5,2,1,2], target = 5,
  • 输出:
    [
    [1,2,2],
    [5]
    ]

1. 分析
本题集合元素会有重复,但要求解集不能包含重复的组合,这就涉及到去重
去重则要分为树枝去重和树层去重
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。
【代码随想录】d30-回溯算法-part06-总结-python_第7张图片
图中橘黄色标注的used的变化,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

2.剪枝
本题仍然只针对和大于target进行剪枝

2.1.5 多个集合求组合

文章链接:https://programmercarl.com/0017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88.html

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
【代码随想录】d30-回溯算法-part06-总结-python_第8张图片
示例:

  • 输入:“23”
  • 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].

说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

1. 分析
本题使用了多个集合来求组合,就不需要从startindex开始遍历了
因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合

树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第9张图片
2.剪枝
本题不需要进行剪枝

2.2分割问题

2.2.1 131.分割回文串

文章链接:https://programmercarl.com/0131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.html

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]

1. 分析
本题难点在于:

  • 切割问题其实类似组合问题
  • 如何模拟那些切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 如何判断回文

如果能想到用求解组合问题的思路来解决 切割问题本题就成功一大半了
本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1
树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第10张图片
2.剪枝
本题不需要进行剪枝,回文子串的判断是题目本身的要求

2.3子集问题

2.3.1 78.子集

文章链接:https://programmercarl.com/0078.%E5%AD%90%E9%9B%86.html

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]

1. 分析
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第11张图片
本题其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了,本来我们就要遍历整棵树。
因为每次递归的下一层就是从i+1开始的,所以不会无限递归。
如果要写终止条件,注意:result.append(path[:]) 要放在终止条件的上面,因为集合本身就是一个子集
2.剪枝
子集问题不需要任何剪枝

2.3.2 子集问题(二)

文章链接:https://programmercarl.com/0090.%E5%AD%90%E9%9B%86II.html

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:

  • 输入: [1,2,2]
  • 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]

1. 分析
本题相比如上一题的子集问题,数组本身是包含重复元素的,但是解集不能包含重复的子集,也就是需要去重
去重方法与**2.1.4 组合总和(三)**操作相同
树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第12张图片

2.3.3 递增子序列

文章链接:https://programmercarl.com/0491.%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97.html

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:

  • 输入: [4, 6, 7, 7]
  • 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

说明:

  • 给定数组的长度不会超过15。
  • 数组中的整数范围是 [-100,100]。
  • 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

1. 分析
本题与**2.3.2 子集问题(二)**很类似,都是求子集+包含重复元素
树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第13张图片
但是要注意,去重一定要先对数组进行排序,但是本题要求递增子序列,排序后所有子序列都是递增的了,所以不能排序
用没有排序的集合{2,1,2,2}来举个例子画一个图,如下:
【代码随想录】d30-回溯算法-part06-总结-python_第14张图片
所以本题的去重需要通过哈希表或者数组来去重,本题主要是为了避免树层重复,那么就可以每层都维护一个哈希表,本层元素出现重复就跳过

2.4排列问题

2.4.1 排列问题(一)

文章链接:https://programmercarl.com/0046.%E5%85%A8%E6%8E%92%E5%88%97.html

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:

  • 输入: [1,2,3]
  • 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

1. 分析
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第15张图片
排列问题与组合等问题的不同点在于:

  • 每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了

2.4.2 排列问题(二)

文章链接:https://programmercarl.com/0047.%E5%85%A8%E6%8E%92%E5%88%97II.html

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:

  • 输入:nums = [1,1,2]
  • 输出: [[1,1,2], [1,2,1], [2,1,1]]

示例 2:

  • 输入:nums = [1,2,3]
  • 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10

1. 分析
本题与**2.4.1 排列问题(一)相比,只有原数组存在重复元素这一定不同,这就涉及到去重,去重逻辑与2.1.4 组合总和(三)2.3.2 子集问题(二)**相同
树形结构如下:
【代码随想录】d30-回溯算法-part06-总结-python_第16张图片

2.5去重问题

2.5.1去重的方法

1. 使用set去重

上面的题目都是统一使用used数组来去重的,其实使用set也可以用来去重!
关键点在于set放的位置

  • 如果放在类成员的位置(相当于全局变量),就把树枝的情况都记录了,不是单纯的控制某一节点下的同一层了。
  • 所以set应该每层都创建一个,只控制本层的元素是否重复(2.3.3 递增子序列的处理方式)

2. 使用startindex去重
是否使用used数组来去重,取决于下一次递归时,是否从0开始,如果递归的时候下一个startIndex是i+1而不是0,那么就不需要使用used数组,

  • 如下图所示,树枝方向,进入下一层startindex取值为i+1,单层for循环本来就是从startindex开始取的,所以一定不满足i>startindex的条件
  • 树层方向,则在满足i > startindex and nums[i] == nums[i-1]时,就代表是重复的元素
    【代码随想录】d30-回溯算法-part06-总结-python_第17张图片
    如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used。

2.5.2 使用set及used数组去重的性能分析

使用set去重的版本相对于used数组的版本效率都要低很多:

  • 程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,
  • 而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。
  • 所以使用set去重,不仅时间复杂度高了,空间复杂度也高了
  • 使用used数组在时间复杂度上几乎没有额外负担!
  • used数组是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。

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