Day25 第七章 回溯演算法part04 回溯終章

haohaoscnblogs發表於2024-08-11

目錄
  • 任務
    • 491.遞增子序列
      • 思路
    • 46. 全排列
      • 思路
    • 47.全排列II
      • 思路
  • 心得體會

任務

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來進行遞迴驅動的。這裡有兩種思考的方法

  1. 考慮用used陣列,表示已經使用的列表元素,每次都可以取沒有使用的列表元素,直到某條路就已經處理完所有元素,這個條件可以用len(path)==len(nums)來表示
  2. 考慮用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) 或者 unKnownFirstlen(nums) 去重方面,同前面問題使用used陣列,注意去重前需要排序。

剩餘題目有時間刷:
332.重新安排行程
51. N皇后
37. 解數獨

相關文章