Day25 第七章 回溯算法part04 回溯终章
任务
491.递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
思路
根据题意,因为要求的就是和顺序有关的子序列,所以不能对原数组进行排序,那么此时就不能用之前的used的方法和i > start的方法去重了,因为这两种方法都需要对原数组进行排序。这里考虑用set横向剪枝。
- 纵向剪枝 由于是递增的,纵向剪枝去掉不符合的情况,就是比较当前值nums[i]和其父节点值path[-1]。
- 横向剪枝 局部变量set加入新增元素,在同一层级的循环中,如果已经有同样的元素,则新的这个节点剪枝,由于是局部变量,不需要显式回溯。
总体逻辑: 取一个节点,下一次再剩余序列中取,递归进行,然后根据子序列至少两个元素,做收集操作。
class Solution: def __init__(self) -> None: self.path = [] self.paths = [] def findSubsequences(self, nums: List[int]) -> List[List[int]]: self.dfs(nums,0) return self.paths def dfs(self,nums,start): if len(self.path)>1: self.paths.append(self.path[:]) size = len(nums) uset = set() for i in range(start,size): if (self.path and nums[i]< self.path[-1]): # 深入时根据条件剪枝(新节点的值小于上一个节点的值) continue if nums[i] in uset: continue uset.add(nums[i]) # 横向剪枝(去重) 由于求的就是有序子序列,不能排序然后像之前那样用 i > start去重,而用局部变量set去重,又由于是局部变量,所以不需要显式回溯,set中的元素表示该层用过的元素 self.path.append(nums[i]) self.dfs(nums,i+1) self.path.pop()
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路
由于全排列选了一个数后,其他的数都可以取,所以不是用start来进行递归驱动的。这里有两种思考的方法
- 考虑用used数组,表示已经使用的列表元素,每次都可以取没有使用的列表元素,直到某条路就已经处理完所有元素,这个条件可以用len(path)==len(nums)来表示
- 考虑用unKnownFirst哨兵的驱动方法,表示当前处理了元素的哨兵,它将原列表分为了两个部分,[0,unKnownFirst)表示已选取的部分,[unKnownFirst,size)表示未选取的部分,随着递归的进行,unKnownFirst最终等于size,即完成了一条路径的选取
# 方法1 used数组 class Solution: def __init__(self) -> None: self.path = [] self.paths = [] def permute(self, nums: List[int]) -> List[List[int]]: used = [False] * len(nums) self.dfs(nums,used) return self.paths def dfs(self,nums,used): if len(self.path) == len(nums): self.paths.append(self.path[:]) return for i in range(0,len(nums)): if used[i]: continue used[i] = True self.path.append(nums[i]) self.dfs(nums,used) self.path.pop() used[i] = False # 方法2 unKnownFirst哨兵 class Solution: def __init__(self) -> None: self.path = [] self.paths = [] def permute(self, nums: List[int]) -> List[List[int]]: self.dfs(nums,0) return self.paths def dfs(self,nums,unKnowFirst): # unKnowFirst 为当前第unKnowFirst的位置的值选哪个 if unKnowFirst == len(nums): self.paths.append(self.path[:]) return for i in range(unKnowFirst,len(nums)): nums[i],nums[unKnowFirst] = nums[unKnowFirst],nums[i] self.path.append(nums[unKnowFirst]) self.dfs(nums,unKnowFirst+1) self.path.pop() nums[i],nums[unKnowFirst] = nums[unKnowFirst],nums[i]
47.全排列II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
思路
由于包含重复元素,且需要返回不重复的全排列,所以需要去重。想到了之前组合中的去重方法,使用used数组进行横向剪枝。
class Solution: def __init__(self) -> None: self.path = [] self.paths = [] self.used = [] def permuteUnique(self, nums: List[int]) -> List[List[int]]: self.used = [False] * len(nums) nums.sort() self.dfs(nums) return self.paths def dfs(self,nums): if un == len(nums): self.paths.append(self.path[:]) return for i in range(0,len(nums)): if self.used[i]: continue if i> 0 and nums[i] == nums[i-1] and self.used[i-1] == False: continue self.used[i] = True self.path.append(nums[i]) self.dfs(nums) self.path.pop() self.used[i] =False
心得体会
暴力递归类的题目目前做过的类型如下:
组合: start 驱动,考虑进入下一层的时候start需要为多少,如下一个数在后续元素中选取。终止条件(收集信息)是到达需求的数量k,此时即为叶子节点,此时收集一条路径的信息,used数组或者i>start条件进行横向去重,注意去重之前要排序
子集: start 驱动,考虑进入下一层的时候start需要为多少,如下一个数在后续元素中选取,终止条件是任意节点,都将它包含的路径加入到最终结果中。递增子序列是一个类子集问题,由于不能排序所以横向剪枝采用局部变量set。
分割: 对比其他的问题相对抽象,start驱动,考虑进入下一层的时候start需要为多少,如下一个分割在后续索引中选取,终止条件为分割索引到达size。[start,i]一般表示该次分割的范围区间。
排列:used驱动,每次只要没选过都可以选。或者unKnownFirst驱动,每次选取的放在unKnownFirst,然后unKnownFirst+1驱动dfs。终止条件为排列完成,len(path)==len(nums) 或者 unKnownFirst ==len(nums) 去重方面,同前面问题使用used数组,注意去重前需要排序。
剩余题目有时间刷:
332.重新安排行程 题意没看懂
51. N皇后 实际同前面就是个暴力递归或者说回溯问题
37. 解数独 与N皇后相似,但是是二维的(两层for循环遍历棋盘,每个位置再用for循环遍历[1,9]选数,不同于之前的暴力递归求结果集,这里只需要找到一个满足条件的棋盘解即可)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步