代码随想录算法训练营第三十一天|491.递增子序列、46.全排列、 47.全排列 II

491.递增子序列

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。

这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的90.子集II (opens new window)

就是因为太像了,更要注意差别所在,要不就掉坑里了!

90.子集II (opens new window)中我们是通过排序,再加一个标记数组来达到去重的目的。

而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。

所以不能使用之前的去重逻辑!

用[4, 7, 6, 7]这个数组来举例

 

回溯三部曲

  • 递归函数参数

本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。

  • 终止条件

本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题! (opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。

但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以大于等于2时才开始收集结果

  • 单层搜索逻辑

本题中不仅要对同层进行去重,还要看遍历的元素是否大于path中的最后一个元素

需要注意,used 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!所以不需要pop

 

class Solution:
    def __init__(self):
        self.path = []  # 用于存储当前递归路径中的元素
        self.result = []  # 用于存储所有符合条件的递归结果

    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        def backtracking(nums, start_index):
            if len(self.path) >= 2:  # 如果当前路径中的元素数量大于等于2
                self.result.append(copy.deepcopy(self.path))  # 将当前路径的深拷贝加入结果中
            used = set()  # 每一次递归即纵向都是新的used,因此不用回溯used
            for i in range(start_index, len(nums)):  # 从start_index开始遍历nums
                if (len(self.path) != 0 and nums[i] < self.path[-1]) or nums[i] in used:  # 如果当前元素小于路径中的最后一个元素或已被使用
                    continue  # 跳过当前元素
                else:
                    self.path.append(nums[i])  # 将当前元素加入路径
                    used.add(nums[i])  # 将当前元素标记为已使用
                backtracking(nums, i + 1)  # 递归调用,继续向下探索
                self.path.pop()  # 回溯,移除路径中的最后一个元素

        backtracking(nums, 0)  # 从索引0开始调用回溯函数
        return self.result  # 返回最终结果

下面这种方式还没有通过,目前还没有找到原因

class Solution:
    def __init__(self):
        self.path = []
        self.result = []
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        def backtracking(nums,start_index):
            if len(self.path) >= 2:
                self.result.append(copy.deepcopy(self.path))
            used = set()
            for i in range(start_index,len(nums)):
                if len(self.path) == 0:
                    self.path.append(nums[i])
                    used.add(nums[i])
                elif len(self.path)!= 0 and nums[i]>= self.path[-1] and nums[i] not in used:
                    self.path.append(nums[i])
                    used.add(nums[i])
                else:
                    continue
                backtracking(nums,i+1)
                self.path.pop()
        backtracking(nums,0)
        return self.result

46.全排列 

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

回溯三部曲

  • 递归函数参数

首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方

可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。

但排列问题需要一个used数组,标记已经选择的元素,未使用的元素都可以再使用

  • 递归终止条件

可以看出叶子节点,就是收割结果的地方。

那么什么时候,算是到达叶子节点呢?

当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

  • 单层搜索的逻辑

这里和77.组合问题 (opens new window)131.切割问题 (opens new window)78.子集问题 (opens new window)最大的不同就是for循环里不用startIndex了。

因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。

而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次

此时可以感受出排列问题的不同

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

class Solution:
    def __init__(self):
        self.path = []  # 初始化一个空列表用于存储当前排列路径
        self.result = []  # 初始化一个空列表用于存储所有排列结果

    def permute(self, nums: List[int]) -> List[List[int]]:
        def backtracking(nums, used):
            if len(self.path) == len(nums):  # 如果当前路径长度等于nums的长度
                self.result.append(copy.deepcopy(self.path))  # 将当前路径的深拷贝添加到结果列表中
                return 
            for i in range(0, len(nums)):  # 遍历nums中的每一个元素
                if used[i]:  # 如果该元素已经被使用
                    continue  # 跳过该元素
                else:
                    self.path.append(nums[i])  # 将该元素添加到当前路径中
                    used[i] = True  # 标记该元素为已使用
                backtracking(nums, used)  # 递归调用回溯函数
                self.path.pop()  # 回溯,移除当前路径中的最后一个元素
                used[i] = False  # 将该元素标记为未使用
        used = [False] * len(nums)  # 初始化一个与nums长度相同的布尔列表,用于标记元素是否被使用
        backtracking(nums, used)  # 调用回溯函数
        return self.result  # 返回所有排列结果

 47.全排列 II

文档讲解:代码随想录

题目链接:. - 力扣(LeetCode)

这道题目和46.全排列 (opens new window)的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列

这里又涉及到去重了。

40.组合总和II (opens new window)90.子集II (opens new window)我们分别详细讲解了组合问题和子集问题如何去重。

那么排列问题其实也是一样的套路。

还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了

 以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:

图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重,并且是在树层上才去重,也就是我们要判断used[i-1]是不是使用过了,如果使用过了,那么就不是在树层上了,如果没有使用过,就是在树层上。因为我们排序过了,所以才可以直接取i-1的值。

一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果

46.全排列 (opens new window)中已经详细讲解了排列问题的写法,在40.组合总和II (opens new window)90.子集II (opens new window)中详细讲解了去重的写法

 代码如下

from typing import List
import copy

class Solution:
    def __init__(self):
        self.path = []  # 初始化一个空列表用于存储当前排列路径
        self.result = []  # 初始化一个空列表用于存储所有排列结果

    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        def backtracking(nums, used):
            if len(self.path) == len(nums):  # 如果当前路径长度等于nums的长度
                self.result.append(copy.deepcopy(self.path))  # 将当前路径的深拷贝添加到结果列表中
                return 
            for i in range(0, len(nums)):  # 遍历nums中的每一个元素
                # 树层去重
                if (i > 0 and nums[i] == nums[i - 1] and used[i - 1] == False) or used[i] == True:
                    # 如果当前元素与前一个元素相同且前一个元素未被使用,或者当前元素已被使用,跳过该元素
                    continue
                else:
                    self.path.append(nums[i])  # 将该元素添加到当前路径中
                    used[i] = True  # 标记该元素为已使用
                    backtracking(nums, used)  # 递归调用回溯函数
                    self.path.pop()  # 回溯,移除当前路径中的最后一个元素
                    used[i] = False  # 将该元素标记为未使用
        nums.sort()  # 对nums进行排序
        used = [False] * len(nums)  # 初始化一个与nums长度相同的布尔列表,用于标记元素是否被使用
        backtracking(nums, used)  # 调用回溯函数
        return self.result  # 返回所有排列结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值