代码随想录算法训练营第二十五天 | 491.递增子序列 46.全排列 47.全排列Ⅱ
LeetCode 491.递增子序列:
文章链接
题目链接:491.递增子序列
思路:
本题是求子集的进一步扩展
本题有几个关键点:
1)要求的是原来数组的非递减序列,所以纵向遍历时要进行判断
2)数组中存在重复元素,需要去重,但是题目要求的非递减序列中元素的排序是按照原来数组中元素的序列来的,因此不能对数组排序后去重。
3)要求非递减子序列中至少有两个元素:只记录深度 >= 2的节点的值(根节点深度为0)
采用的去重方法:去重只在当前层进行去重,也就是for循环中,因此需要一个数据结构,记录当前递归的for循环中已经遍历到的元素。查看元素在之前是否出现过,采用哈希表
① hash表采用set
② 因为num[i]的范围为[-100, 100]有限区间,因此可以采用数组作为hash表,num[i]与下标的映射关系为 k = nums[i] + 100。数组元素个数为200, 定义时应该是hashlist[201]
对应的树如下,其中红色部分是重复读取的部分,即去重;紫色部分是不符合非递减序列的部分
三部曲:
① 参数:startIndex记录开始,depth记录当前节点路径,path记录从根节点到当前节点的路径
def backtracking(self, nums, startIndex, depth, path):
② 边界条件:startIndex >= len(nums),
③ 单层搜索逻辑:需要注意加入的值是否构成非递减序列,以及去重问题
PS:子集问题需要记录树中非叶节点和叶子节点,因此需要先记录当前节点的值
class Solution:def findSubsequences(self, nums: List[int]) -> List[List[int]]:if len(nums) == 1:return []self.result = []self.backtracking(nums, 0, 0, [])return self.resultdef backtracking(self, nums, startIndex, depth, path):if depth >= 2: # 记录当前节点self.result.append(path[:])if startIndex >= len(nums):return# uset = set() # 采用sethashlist = [0] * 201 # nums[i] + 100对应hashlist的下标,nums[i]范围为[-100, 100]for i in range(startIndex, len(nums)):if path != [] and path[-1] > nums[i]: # 要加入的值 < path[-1]continueif hashlist[nums[i] + 100] == 1: # 去重continue# if nums[i] in uset: # 去重# continue# uset.add(nums[i])hashlist[nums[i] + 100] = 1path.append(nums[i])self.backtracking(nums, i + 1, depth + 1, path)path.pop()
感悟:
需要分析清楚题目中的各个关键点。
LeetCode 46.全排列:
文章链接
题目链接:46.全排列
思路:
首先明确题目要求是数组的元素的全排列,相同元素的集合,元素顺序不同就是不同的排列,比如[1, 2, 3]与[3, 2, 1]是不同的排列。
全排列构成的树深度为数组元素的个数,每一层是确定对应位置放哪个元素。
对应的树如下,我们要取的是叶子节点的值。并且每次纵向传递时,比如[1,2,3],选择了2,下一层递归需要传递的是[1, 3]而不只是[3]。
① 参数:nums[],path记录路径,used[]数组记录nums中的元素在前面是否使用过
② 边界条件:叶子节点,即len(path) == len(nums)
③ 单层递归:遍历nums的全部元素,取之间没有用过的元素作为孩子节点的值
"""
使用used数组记录
"""
class Solution:def permute(self, nums: List[int]) -> List[List[int]]:self.result = []used = [False] * len(nums)self.backtracking(nums, [], used)return self.resultdef backtracking(self, nums, path, used):if len(path) == len(nums):self.result.append(path[:])returnfor i in range(len(nums)):if used[i]: # 这个数字在前面用过了continueused[i] = Truepath.append(nums[i])self.backtracking(nums, path, used)path.pop()used[i] = False"""
由于python中list可以切片访问,因此可以把nums去除nums[i]后传递给下一层
"""
class Solution:def permute(self, nums: List[int]) -> List[List[int]]:self.result = []self.backtracking(nums, [])return self.resultdef backtracking(self, nums, path):if nums == []:self.result.append(path[:])returnfor i in range(len(nums)):path.append(nums[i])self.backtracking(nums[0:i] + nums[i+1:], path)path.pop()
感悟:
全排列要传递给下一层是nums除nums[i]外的元素,也可以使用used数组记录前面使用过的元素。以及排列与元素的位置有关
LeetCode 47.全排列Ⅱ:
文章链接
题目链接:47.全排列Ⅱ
思路:
全排列+去重
去重前先排序,这样才能通过相邻元素是否相等来去重
对应的树如下。我们需要得到的也是树的叶子节点,本题中去重采用同树层去重的方式
① 参数:nums,path记录路径,used记录nums中的元素是否使用过
② 边界条件:叶子节点,len(path) == len(nums)
③ 单层搜索逻辑:搜索nums中的全部元素,需要跳过之前使用过的元素和同树层的相同元素
class Solution:def permuteUnique(self, nums: List[int]) -> List[List[int]]:self.result = []used = [False] * len(nums)nums.sort() # 排序排序排序self.backtracking(nums, used, [])return self.resultdef backtracking(self, nums, used, path):if len(path) == len(nums):self.result.append(path[:])returnfor i in range(len(nums)):if used[i]: # 在前面用过了continue# used[i-1] = False,nums[i-1]是同一树层的节点,nums[i]节点需要去掉# used[i-1] = True,nums[i-1]是同一树枝上的节点,也就是nums[i]的祖先节点,不需要去掉if i > 0 and nums[i] == nums[i - 1] and used[i - 1] == False:continueused[i] = Truepath.append(nums[i])self.backtracking(nums, used, path)path.pop()used[i] = False
感悟:
使用相邻元素是否相等来去重的需要先排序
回溯的题目很适合先构建树,再完善代码
学习收获:
① 去重的两种类型:1)相邻元素相等去重,先排序,再使用 i > startIndex 或 used记录从根到当前节点,哪些元素使用过。2)相同元素之前是否出现过,去重,使用哈希表,常用的是数组和set,记录当前递归的for循环中哪些元素使用过
② 组合、排列要得到的是树的叶子节点,求子集要得到的树的全部节点(可能因为有些节点不符合条件就跳过了)
③ 全排列,排列与元素位置有关,且不需要用startIndex。[1, 2, 3]选择了[2]需要传递[1, 3],因此使用used数组记录之前使用过的元素