合法括號對之三種解法


  這道題目是leetcode上的一個題目,算是一道小題,但是解法很多,特此總結。

題目描述

  給定一個只包含 ‘(’ 和 ‘)’ 的字符串,找出最長的包含有效括號的子串的長度。

示例 1 示例 2
輸入 “(()” “)()())”
輸出 2 4
解釋 最長有效括號子串爲 “()” 最長有效括號子串爲 “()()”

題目分析

  回想以前的操作,可能都會用棧來判斷一個字符串是否是合法的括號序列,這個複雜度是的O(n)O(n),而一個長度爲n的字符串一共可以有n2n^2個子串,對每一個都判斷,時間複雜度也就是O(n3)O(n^3)的,這個就是暴力方法。
  暴力法中相當於對很多子串都重複判斷了是否是合法的括號。所以我們思考其他方法。

動態規劃解法

  我們模仿最大子串和的方法,我們定義dpidp_i爲到第i個字符爲止,最長的合法的括號子串長度。我們通過這個設置來簡化問題。首先,如果第i個字符是’(’,則此時顯然不存在合法的字符串,因爲不會有一個合法的括號序列時候以左括號結尾的,dpi=0dp_i=0。如果第i個字符是’)’,這個時候有可能會增加最長合法括號子串的長度。如果前一個字符是’(’,那麼一定會有合法的子串,且長度是dpi2+2dp_{i-2}+2,如果i>1的話。如果i=1,顯然不需要繼續向dpi2dp_{i-2}追溯。
  但是如果前一個字符也是右括號,那麼這個時候情況就複雜了。因爲有可能前面的字符已經是非法的括號子串了,這個時候,再多一個’)’,還是非法的,dpi=0dp_i=0,但是如果前面的字符已經和之前的匹配了,這個時候‘)’如果要和左括號匹配,只能從前一個字符之前未匹配的左括號中找。舉個例子’(()))’,第二個右括號前面的右括號已經匹配了第二個左括號,所以第二個右括號要匹配的左括號只能是前面的右括號所匹配的左括號(第二個左括號)之前的字符,也就是第一個左括號。
  此時,如果這個右括號和前面的左括號匹配了,這個時候就有可能把再前面的字符串連接起來,此時的長度就是dpi=dpi1+2+dp(idpi12)dp_i=dp_{i-1}+2+dp_{(i-dp_{i-1}-2)},這裏還是需要判斷一下下標是否越界。

  總結一下所有的情況,看到動態規劃的情況就是下面的樣子。這裏默認所有的dpi=0i<1dp_i=0 \quad i<1,下標爲負數,值爲0。
在這裏插入圖片描述
  這個動態規劃最複雜的問題就是各種條件判斷,基本的條件判斷都是要同時看兩個字符,而且甚至有些情況開始套娃了。但是總之查看過去的狀態數是常數,不超過兩個。理清了思路可以寫代碼了。

python代碼如下
def longestValidParentheses_dp(s: str) -> int:
    if not s: return 0
    dp = [0] * len(s)
    maxLength = 0
    for i in range(1, len(s)):
        if s[i] == ')':
            if s[i - 1] == '(':
                dp[i] = dp[i - 2] + 2 if i > 1 else 2
            elif i - dp[i - 1] > 0 and s[i - dp[i - 1] - 1] == '(':
                dp[i] = dp[i - 1] + 2
                if i - dp[i - 1] - 1 > 0:
                    dp[i] += dp[i - dp[i - 1] - 2]
            maxLength = max(maxLength, dp[i])
    return maxLength

  顯然動態規劃只需要對字符串掃描一遍,時間複雜度是O(n)O(n)的,因爲要保存以前的值,所以空間複雜度也是O(n)O(n)的。

棧的方法

  如果我們對棧匹配括號的算法進行改進一下,我們就能解決這個問題,但是這個方法同樣是判斷條件比較多一些。這個解法就比較巧妙一些,我們直接使用棧來處理這個問題,左括號入棧,右括號出棧匹配,一個最大的難點就是後面匹配的時候出棧了,和前面匹配的序列不好連接起來,每次匹配都是到右括號結束,但是匹配之後入棧的左括號也全都匹配了,這個時候就需要把這部分連起來,直觀的思路比較難以處理這個地方。
  我們可以通過在棧中保存左括號的索引來解決這個問題,我們不知道這個序列一直可以向前連到什麼地方,但是我們可以看棧頂元素的索引,顯然棧頂元素右邊的序列肯定是已經完成了匹配,這個時候我們通過做差來求解匹配的長度。
  如果先前進去的括號都匹配了,這個時候我們無法查看棧頂元素,爲了把操作統一起來,我們初始化棧的時候在裏面加上-1,如果它在棧頂,表示0-i的括號完成了匹配,長度依舊是i-(-1)。
  但是如果是右括號來了,結果把-1給彈棧了,這個時候是匹配失敗啊,我們就採取一種方法,把i入棧,來把彈出去的-1替代下來,這樣也方便後面求取長度。後面再遇到無法匹配的右括號也是同理,只要判斷棧是否爲空即可。

python代碼
def longestValidParentheses(s: str) -> int:
    maxLength = 0
    stack = [-1]
    for i, c in enumerate(s):
        if c == '(':
            stack.append(i)
        else:
            stack.pop()
            if len(stack)==0:
                stack.append(i)
            else:
                maxLength = max(maxLength, i - stack[-1])
    return maxLength

兩遍掃描,空間O(1)O(1)的解法

  這種方法我承認自己想不到,看到之後感覺真的耳目一新。但是這種方法可能過於對問題有針對性,本文所述的三種算法普適性是逐漸減弱的,這種方法大家就當擴充眼界吧。
  利用兩個計數器 left和 right。首先,我們從左到右遍歷字符串,對於遇到的每個 ‘(’,我們增加 left計算器,對於遇到的每個‘)’,我們增加 right 計數器。如果 right 計數器比 left 計數器大時,我們將 left 和 right 計數器同時變回 0 。此外每當 left 計數器與 right計數器相等時,我們計算當前有效字符串的長度,在這種情況下,左括號數目始終不小於右括號,兩者數目相等時一定匹配。
  但是這種情況下會不會漏掉什麼情況呢,比如’(()’,顯然有一對合法的括號,但是我們是無法對其計數的。所以我們要排除這種特殊情況。
  我們從需要右到左做一遍類似的工作,但是這一次是如果左括號數目大於右括號,我們就從新計數。原理和從左到右是類似的。這樣總共需要兩趟掃描,時間複雜度是O(n)O(n),僅保存常數個變量,空間複雜度O(1)O(1)

python代碼
def longestValidParentheses_twoscan(s: str) -> int:
    left = right = maxLength = 0
    for c in s:
        if c == '(':
            left += 1
        else:
            right += 1
        if left == right:
            maxLength = max(right << 1, maxLength)
        elif right > left:
            left = right = 0
    left = right = 0
    for c in s[::-1]:
        if c == '(':
            left += 1
        else:
            right += 1
        if left == right:
            maxLength = max(right << 1, maxLength)
        elif right < left:
            left = right = 0
    return maxLength
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章