代码随想录算法训练营第三十天 | 回溯算法 part 6 | 总结

回溯框架

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

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

组合问题

组合

组合问题最基础的就是,给定两个整数nk,求1...n中所有可能的k个数的组合。

使用如上框架即可写出来。

注意的是,组合需要使用startIndex来保证不重复选取。

在这里插入图片描述

剪枝

剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。

for i in range(startIndex, n - (k - len(path)) + 2):

组合求和 i

这个是在组合基础上加了求和的一个要求。

反而更简单了,因为有求和可以方便剪枝。

在这里插入图片描述

已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉

组合求和 ii

这道题和组合求和i的区别是,此题的元素可以重复选取。但是这两题都不包含重复的元素。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

如果是一个集合来求组合的话,就需要startIndex,例如组合求和i

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如电话号码的字母组合

和组合求和i的区别在于,进入下一次递归的时候,传递的startIndex不需要+1,因为可以对自身重复选取。

在这里插入图片描述
本题的剪枝优化:先对candidates进行排序,随后需满足sum + candidates[i] <= target

组合求和 iii

本题包含重复元素,但是不可重复选取。要求解集中不包含重复的组合。

树层去重

在这里插入图片描述

图中used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

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

子集问题

子集i

子集问题相当于求组合问题中的所有遍历节点。

不需要写终止条件,因为需要遍历全部。

不会无限递归,因为下一层的递归我们传递的是i + 1

在这里插入图片描述

子集ii

nums中包含重复的元素,但是解集中不能出现重复的子集。

所有此题需要去重。先排序,然后可以使用used数组,或者指针去重。

树层去重

在这里插入图片描述

递增子序列

本题不能对nums进行排序,所以在树层可以用set来去重。

在这里插入图片描述

排列问题

排列i

此题排列我是用slicing写的,因为没有重复元素所以可以这样写,并没有用used数组,但是下一题去重是需要用到used数组的。

在这里插入图片描述

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

排列ii

这道题包含重复的元素,对其进行排列,但是解集不能含有重复的排列。

这道题我思考了很久。首先,因为要去重,所以nums需要排序。

在这里插入图片描述

used数组在这道题有两个作用:

  1. 树层去重
  2. 保证每次循环的时候不重复地从集合中拿元素出来排序。(从根节点到叶节点的过程)

这道题和组合求和iii的区别:

组合求和iii在循环的时候使用startIndex,下一次循环的时候不需要重新遍历之前的元素,所以不需要用到used数组。在组合求和iii的去重环节,使用
if i > index and candidates[i] == candidates[i-1]: continue便可以完成去重。

但是排列ii是需要在每一次循环中遍历所有数组,故需要used数组来识别已经遍历过的元素。

该题是如何保证不包含重复排列的?

还是以上图为例子。假设最后得到的排列集为P = {p1, p2, p3…}。

  1. 每一层树层,对应的是所得到排列的某个位置。比如说第一层树层,对应的是p1[0](或者p2[0]…)。所以树层去重能够保证对于每个元素在一个位置的情况,不会重复考虑。比如说1p1[0]的位置上可以在树状图延伸出{1,1,2}{1,2,1},如果不做树层去重的话就会延伸出两份{1,1,2}{1,2,1}

  2. used数组保证了在一个树枝的遍历上不会重复选取集合中的元素。

代码贴出来方便回忆:

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        nums.sort()
        used = [False] * len(nums)
        def helper():
            if len(path) == len(nums):
                res.append(path.copy())
                return

            for i in range(len(nums)):
            	# used 作用 1
                if i > 0 and nums[i] == nums[i-1] and used[i-1] == False:
                    continue
                # used 作用 2
                if used[i]:
                    continue
                used[i] = True
                path.append(nums[i])
                helper()
                path.pop()
                used[i] = False
                
        helper()
        return res  

复杂度分析

  • 子集问题分析:

    • 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
    • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
  • 排列问题分析:

    • 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!
    • 空间复杂度:O(n),和子集问题同理。
  • 组合问题分析:

    • 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
    • 空间复杂度:O(n),和子集问题同理。
  • N皇后问题分析:

    • 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * .... * 1
    • 空间复杂度:O(n),和子集问题同理。
  • 解数独问题分析:

    • 时间复杂度:O(9^m) , m'.'的数目。
    • 空间复杂度:O(n^2),递归的深度是n^2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值