LeetCode22 生成所有括號對


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


鏈接

Generate Parentheses


難度

Medium


描述


Given n pairs of parentheses, write a function to generate all combinations
of well-formed parentheses.

給定n對括號,要求返回所有這些括號組成的不同的合法的字符串

For example, given n = 3, a solution set is:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

題解


這道題目非常有意思,解法也很多,還是老規矩,我們先由易到難,先從最簡單的方法開始入手。

我們來簡單分析一下題目,n個括號對意味着字符串的長度是2n,我們利用排列組合可以計算出,所有的組合種數一共有\(C_{2n}^n\)種。算一下會知道,這個數是很大的,也就是說我們哪怕一開始就知道答案,把答案遍歷一遍也會有很高的耗時,所以這道題對於時間復雜度的要求應該不會很高。


暴力


能想到最簡單的方法,當然是暴力,不要看不起這個朴素的算法,很多時候靈感都是從暴力當中獲取的。但是這道題暴力不太容易寫,因為會有一種無從入手的感覺,我們知道要暴力,但是並不知道應該怎樣暴力。這道題不存在可以直接枚舉的朴素元素,必須要我們拐個彎才行。

怎么拐彎呢,其實答案我剛才已經說出來了。n個括號對,也就是說一共2n個字符,我們可以枚舉n個'('分別放在什么位置,剩下的自然就是')'了。看起來很有道理,但是有一個問題,就是這個思路並沒有辦法通過循環直接實現。這其實已經進化成了一個搜索問題了,我們要搜索所有可以擺放括號的可能性。

如果你能從暴力方法跳躍到搜索問題,那么說明你離寫出代碼已經很接近了。如果不行,那么我建議你花點時間去學習一下搜索算法專題。

對於搜索問題而言,這已經很簡單了,我們搜索的空間是明確的,2n個位置,搜索的內容,對於每個位置我們可以擺放'('也可以擺放')'。那么代碼自然而然呼之欲出:

def dfs(pos, left, right, n, ret, cur_str):
    """
    pos: 當前枚舉的位置
    left: 已經放置的左括號的數量
    right: 已經放置的右括號的數量
    n: 括號的數量
    ret: 放置答案的數組
    cur_str: 當前的字符串
    """
    if pos == 2*n:
        ret.append(cur_str)
        return 
    if left < n:
        dfs(pos+1, left+1, right, n, ret, cur_str+'(')
    if right < n:
        dfs(pos+1, left, right+1, n, ret, cur_str+')')

這個程序遍歷運行之后還沒有結束,我們還需要判斷生成出來的括號是否合法,也就是說括號需要匹配上。我們可以用一個棧來判斷括號是否能夠匹配,比如我們遇見左括號就進棧,遇見右括號則判斷棧頂,如果棧頂是左括號,那么棧頂的左括號出棧,否則則入棧,最后判斷棧是否為空。這個算法實現當然不難,但是如果你仔細去想了,你會發現完全沒有必要用棧,因為如果我們遇到右括號的時候,棧頂不為左括號,那么一定最后是無法匹配的。因為后面出現的左括號不能匹配前面出現的右括號,正所謂往者不可追就是這個道理。【狗頭】


優化


我們來思考一個問題:什么情況會出現右括號遇不到左括號呢?只有一種情況,就是當前出現右括號的個數超過了左括號,也就是說我們遍歷一下字符串,如果中途出現右括號數量超過左括號的情況,那么就說明這個字符串是非法的。看起來沒毛病對吧,但是有問題,我們為什么不在枚舉的時候就判斷呢,如果左括號放入的數量已經等於右括號了,那么就不往里防止右括號,這樣不就可以保證搜索到的一定是合法的字符串嗎?

如果你能想到這一層,說明你對搜索的理解已經很不錯了。我們看一下改動之后的代碼:

def dfs(pos, left, right, n, ret, cur_str):
    """
    pos: 當前枚舉的位置
    left: 已經放置的左括號的數量
    right: 已經放置的右括號的數量
    n: 括號的數量
    ret: 放置答案的數組
    cur_str: 當前的字符串
    """
    if pos == 2*n:
        ret.append(cur_str)
        return 
    if left < n:
        dfs(pos+1, left+1, right, n, ret, cur_str+'(')
    if right < n and right < left:
        dfs(pos+1, left, right+1, n, ret, cur_str+')')

大部分代碼都沒有變化,只是在right < n后面加入了一個right < left這個條件。看似只有一個條件,但是這個條件起到的作用至關重要。整個算法的效率有了質的提升,實際上這也是效率最高的算法。


構造


上面的方案在LeetCode官方當中都有收入,也是比較常規的解法,下面要介紹的方法是我的原創,我個人感覺也比較有意思,分享給大家。

在之前的文章當中我們介紹過分治法,分治法的核心是將一個看似無法求解的大問題,分解成比較容易解決的小問題,最后加以解決。這道題當中,我們直接求n時的解法是比較困難的,沒辦法直接獲得,我們能不能也試着使用分治的方法來解決呢?

我們來觀察一下數據,當n=1的時候,很簡單,結果是(),只有這一種。當n=2呢?有兩種,分別是(())和()(),當n=3呢?有5種:((())), ()(()), ()()(), (()()), (())()。這當中有沒有規律呢?

我們用solution(n)表示n對應的解法,那么我們可以寫出solution(n)對應的公式:

\[solution(n) = \sum_{i=1}^{n-1} solution(i)+solution(n-i) + ( + solution(n-1) + ) \]

上面這個式子有點像是動態規划的狀態轉移方程,雖然不完全一樣,但是大概是那么回事。也就是說我們可以用比答案規模小的答案組裝成現在的答案。比如n=3時的答案,等於n=2時的答案和n=1時答案的拼接。

比如: solution(1) + solution(2) 可以得到: ()()()和()(()),solution(2) + solution(1)可以得到 ()()()和(())()。但是還有一種答案無法通過拼接得到就是( solution(2) )。也就是說在solution(2)的答案外面包一層括號。那為什么不用考慮solution(1)的答案外面包兩層括號呢?答案很簡單,因為solution(2)已經包括了這樣的情況,所以我們只用往下考慮一層。

不過還沒有結束,還有一點小問題,就是這樣得到的答案可能會有重復,所以我們需要去重,利用set我們可以很簡單做到這點,讓我們一起來看代碼:

class Solution:
            
    def generateParenthesis(self, n: int) -> List[str]:
        solutionMap = {}
        # 記錄下n=0和1時的答案
        solutionMap[0] = set([""])
        solutionMap[1] = set(["()"])

        # 遍歷小於n的所有長度
        for i in range(2, n+1):
            cur = set()
            # 遍歷小於n的所有長度
            for j in range(1, i):
                # 構造答案
                ans1 = solutionMap[j]
                ans2 = solutionMap[i-j]
                for s in ans1:
                    for t in ans2:
                        cur.add(s + t)
            # 構造 ( solution(n-1) )這種答案
            for s in solutionMap[i-1]:
                cur.add("(" + s + ")")
            solutionMap[i] = cur
        return list(solutionMap[n])

在C++當中,這兩種方法的效率差不多,但是使用Python的話,構造的方法要更快一些。和搜索這種方法相比,搜索是不知道答案去搜尋答案,而構造法是知道答案大概長什么樣子,依據一定的規則生產答案。可以說是兩種不同思路的解法,也是我本人很喜歡這道題的原因。

這道題的代碼都不長,但是思路挺有意思,希望大家會喜歡。

今天的文章就是這些,如果覺得有所收獲,請順手掃碼點個關注吧,你們的舉手之勞對我來說很重要。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM