LeetCode46 回溯演算法求全排列,這次是真全排列

承志發表於2020-04-05

本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是LeetCode的26篇文章,我們來實戰一下全排列問題。

在之前的文章當中,我們講過八皇后、回溯法,也提到了全排列,但是畢竟沒有真正寫過。今天的LeetCode46題正是讓我們生成給定元素的全排列。

題意很簡單,只有一句話,給定一個沒有重複元素的序列,讓我們返回這個序列所有的全排列,並且我們不需要考慮這些排列的順序。

回溯法

我們在之前的文章當中分析過,全排列問題,可以看成是搜尋問題,從而近似成八皇后問題。在八皇后問題當中,我們列舉的是棋盤的每一行當中的皇后放置的位置,而全排列其實也一樣,我們要列舉每一個元素放置的位置。不過八皇后當中要求皇后除了不能同行同列之外還不能同對角線,而我們排列元素可以忽略這個要求。也就是說我們把每一行皇后放置的列號看成是每個元素擺放的位置,並且忽略同對角線的限制的話,那麼八皇后問題和全排列問題就完全一樣了。

如果還不理解,可以參考一下下圖,我們給皇后編號,把皇后同樣看成是序列當中的元素,那麼八皇后的擺放位置剛好可以對映成一種排列。對映的方式非常簡單,就是我們忽略行的資訊,依次記錄下皇后擺放的列號。

如果你能想通這兩個看似完全不同的問題當中的相似之處,說明你對搜尋問題的理解已經有些入門了。

思路清楚了,總之我們要列舉皇后擺放的狀態。你可以按順序遍歷位置,然後列舉各個位置上放置的皇后,也可以順序遍歷皇后,列舉當前皇后可以放置的位置。兩者是等價的,你可以根據自己的理解進行操作。

一般來說我喜歡遍歷位置,列舉皇后。因為會引起衝突的是皇后,而不是位置。我們往往要判斷皇后之間的關係以及皇后的狀態,所以我們列舉皇后會比較貼合思路

所以我們把之前八皇后的程式碼拿過來稍作修改即可,為了放置一個皇后重複放置在多個位置,我們需要儲存皇后的狀態,即有沒有放置過。一般競賽當中這種標記的變數稱為flag,如果標記多個那就是flag陣列。更多細節我們來看程式碼:

class Solution:
    def dfs(self, nums, n, i, cur, ret, flag):
        if i == n:
            ret.append(cur.copy())
            return
        for p in range(n):
            # 遍歷所有元素
            # 如果p元素已經放置過了,跳過
            if flag[p]:
                continue
            # 當前位置放置p
            cur.append(nums[p])
            # flag[p]置為True
            flag[p] = True
            # 遞迴
            self.dfs(nums, n, i+1, cur, ret, flag)
            # 回溯
            cur.pop()
            flag[p] = False
        
    def permute(self, nums: List[int]) -> List[List[int]]:
        ret = []
        n = len(nums)
        # 記錄元素i有沒有放置過
        flag = [False for _ in range(n)]
        self.dfs(nums, n, 0, [], ret, flag)
        return ret
複製程式碼

程式碼很短,細節也不多,只要理解了我們是按照順序遍歷位置,然後對於每一個位置遍歷可以放置的元素,然後遞迴回溯即可。基本上可以說是模板題,如果理解有難度的話,可以看一下之前詳解八皇后問題的文章:

LeetCode 31:遞迴、回溯、八皇后、全排列一篇文章全講清楚

其他方法

回溯法是這個問題的標準解法,那麼這題還有沒有其他方法呢?

其實是有的,也不難,在LeetCode31題的文章,也就是上面那個連結的文章當中我們解決了一個叫做下一個排列的問題。在這道題當中,我們給定一個序列,要求返回在它所有的全排列當中剛好字典序比它大1的排列,這個方法稱為next_permutation。

關於next_permutation的計算方法也在連結裡,如果有忘記的或者是最近關注的可以點下連結回顧一下,計算方法是完全一樣的,我就不再重複了。

LeetCode 31:遞迴、回溯、八皇后、全排列一篇文章全講清楚

如果還記得這道題的話就好辦了,我們使用它很容易解出當前的問題。因為我們只需要獲得給定序列的最小排列,然後不停地呼叫這個方法就好了,直到沒有更大的序列退出即可。從最小的序列一直獲取到最大的,當然就是全排列了。

在LeetCode31題當中,這是一個inplace的方法,沒有返回值。並且當序列達到最大的時候,會自動再從最小的開始。我們需要稍稍修改一下,加上一個返回值,表示當前的序列是否是最大的。如果序列達到最大,說明我們可以不用繼續往下尋找了,我們return一個True,表示可以退出了,否則我們return False,表示還有其他結果。

本質上我們是從最小的排列開始,不停地用一個叫做get_next的方法獲取比當前序列大的下一個序列,當沒有更大的序列的時候,說明我們已經獲得了所有的排列,那麼直接返回結果即可。如果忽略get_next當中的邏輯,這個程式碼其實只有幾行:

其實這是一個取巧的辦法,利用之前的思路我們完全不用思考,幾乎可以無腦得到答案。但是從另外一個角度來說,這也是演算法的魅力,畢竟通往終點的路往往不止一條。

最後我們來看下程式碼,如果你不懂怎麼算next_permutation光看註釋是很難看懂的,劃到上面的連結看看吧。

class Solution:
    def get_next(self, nums: List[int]):
        """
        Do not return anything, modify nums in-place instead.
        """

        # 長度
        n = len(nums)
        # 記錄圖中i-1的位置
        pos = n - 1
        for i in range(n-10-1):
            # 如果降序破壞,說明找到了i
            if nums[i] > nums[i-1]:
                pos = i-1
                break
                
        for i in range(n-1, pos, -1):
            # 從最後開始找大於pos位置的
            if nums[i] > nums[pos]:
                # 先交換元素,在進行翻轉
                nums[i], nums[pos] = nums[pos], nums[i]
                # 翻轉[pos+1, n]區間
                nums[pos+1:] = nums[n:pos:-1]
                return False
        return True
        
        
    def permute(self, nums: List[int]) -> List[List[int]]:
        ret = []
        # 從小到大排序,獲得最小排列
        nums = sorted(nums)
        ret.append(nums.copy())
        # 如果還有下一個排列則繼續呼叫
        while not self.get_next(nums):
            # 要.copy()是因為Python中儲存的引用,如果不加copy
            # 會導致當nums發生變化之後,ret中儲存的資料也會變化
            ret.append(nums.copy())
        return ret
複製程式碼

今天的問題並不難,只是Medium難度,並且題目的題意還是之前見過的,主要是給大家加深一下回溯演算法的映像用的,沒什麼太多的新內容。

文章的內容就是這些,如果覺得有所收穫,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。

LeetCode46 回溯演算法求全排列,這次是真全排列

相關文章