1. 程式人生 > >LeetCode 32,並不Hard的難題,解法超級經典,帶你領略動態規劃的精彩

LeetCode 32,並不Hard的難題,解法超級經典,帶你領略動態規劃的精彩

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


今天給大家分享的是LeetCode當中的32題,這是一道Hard難度的題。也是一道經典的字串處理問題,在接下來的文章當中,我們會詳細地解讀有關它的三個解法。

希望大家不要被題目上的標記嚇到,雖然這題標著難度是Hard,但其實真的不難。我自信你們看完文章之後也一定會這麼覺得。


連結

Longest Valid Parentheses

難度

Hard


描述

給定一個只包含左右括號的字串,返回最長能夠組成合法括號的長度

Given a string containing just the characters '(' and ')', find the length
of the longest valid (well-formed) parentheses substring.

樣例 1:

Input:  "(()"
Output: 2
## Explanation: The longest valid parentheses substring is "()"

樣例 2:

Input:  ")()())"
Output: 4
## Explanation: The longest valid parentheses substring is "()()"


思考


我們來分析一下題目,這題的題目很容易理解,本質上是一個尋找字串最大子串的問題。合法的條件是括號合法,也就是左右括號能夠對上。我們之前分析過左右括號的合法條件,其實很簡單,我們只需要保證右括號出現的數量一直小於等於左括號的數量。

也就是說((()是有可能合法的,而())是一定不合法的。原因很簡單,因為如果左括號的數量大於右括號,那麼由於後續還可能會有括號出現,所以還是有可能構成合法的。而反之則不行,所以如果右括號的數量過多,那麼一定不合法。


暴力


根據我們上面分析的結果,我們不難想出暴力的解法。我們可以列舉所有的字串的位置,作為開頭,接著找出從這個開頭開始可以達成合法的最大的長度。

我們前面說了如果(出現的數量大於)的時候,後面仍然可能構成合法的匹配,所以我們不能結束,還需要往下繼續搜尋。而如果)的數量超過(那後面的就可以不用看了,直接退出即可。如果(的數量等於),那麼說明可以構成匹配,我們試著更新答案。

我們用程式碼實現這個邏輯:

def brute_force(s):
    ans = 0
    for i in range(len(s)):
        if s[i] == '(':
            l = r = 0
            for j in range(i, len):
                if s[j] == '(':
                    l++
                else:
                    r++
                # 如果已經不可能構成答案
                if r > l:
                    break
                if r == l:
                    ans = max(ans, 2 * r)
    return ans

這段程式碼應該都能看懂,我們只需要判斷非法和合法兩種情況,如果非法則退出迴圈,列舉下一個起始位置。如果合法,則試著更新答案。最後,我們返回最大的結果。

這個暴力的解法當然沒問題,但是顯然複雜度不夠完美,還有很大提升的空間。而且如果這題就這麼個難度,那麼也肯定算不上是Hard了。接下來要給大家介紹一種非常巧妙的方法,它不會涉及許多新的演算法和知識點,只是和之前的題目一樣,需要我們對問題有比較深入的理解。


尋找模式法


接下來要介紹的這個方法非常硬核,我們不需要記錄太多的資訊,也不會用到新奇的或者是高階的演算法,但是需要我們仔細分析問題,找到問題的“模式”。我把它稱作是模式匹配演算法。

其真實模式匹配是專有名詞,我這裡只是借用一下。它有些像是正則表示式,我們寫下一個模式匹配的規則,然後正則表示式引擎就可以根據我們寫下的模式規則去尋找匹配的字串。比如說我們希望找到所有的郵箱,我們約定一個模式,它接受一個3到20的字串,中間必須要存在一個@符號,然後需要一個域名作為字尾。

我們剛才對於一個郵箱地址的描述,包括長度、內容以及必須存在的字元等等這些要求其實就是模式。這個概念有些抽象,但是並不難理解,我相信你們應該都能明白。理解了這個概念之後,我們來思考一個問題,在這個問題當中,最終長度最大的答案它的模式是什麼?我們稍微想一下就可以想明白,不論它多長,它裡面的內容是什麼樣,它應該是以(為開頭,以)為結尾。

我們把這個答案命名為t,我們繼續來思考,t前面和後面的一個符號的組合會是什麼樣的?

我們列舉一下就能知道,一共只有3種情況,分別是(t(,)t)和)t(。(t)是不可能的,因為這樣可以組成更長的答案,這和我們一開始的假設矛盾了。所以只有這三種情況。

我們來關注一下)t)和)t(這兩種情況,對於這兩種情況來說,我們可以肯定一點,t前面的)一定不是一個合法括號的結尾。答案也很簡單,如果)能夠構成合法的括號匹配,那麼答案的長度顯然也會增加。所以它一定是在一個非法的位置,既然出現在非法的位置,那麼我們就可以忽略。換句話說,對於這兩種情況而言,我們只需要遍歷一次字串,維護構成的合法括號的位置,就一定可以找到它們。

原因也很簡單,在我們遍歷到了t前面的)的位置的時候,由於非法,我們會將所有記錄的左右括號的資訊清除。所以我們一定可以順利地找到t,並且不會受到其他符號的干擾。

但是這樣只能包含兩種情況,對於(t(的情況我們怎麼處理呢?因為是左括號,我們無法判斷它的出現是否會產生非法。也就是說我們在遍歷的時候,無法將t前面的左括號帶來的影響消除。對於這個問題其實很簡單,我們只需要反向遍歷即可。由於我們遍歷的順序翻轉,所以(成了可能構成非法的符號,而)不是,於是就可以識別這一種情況了。

我們寫出程式碼,真的很簡單,只有兩次遍歷陣列:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        ans = 0
        l, r = 0, 0
        # 正向遍歷,尋找)t( 和 )t(兩種情況
        for i in range(n):
            if s[i] == '(':
                l += 1
            else:
                r += 1
                if r > l:
                    l, r = 0, 0
                elif r == l:
                    ans = max(ans, l + r)
                    
        l, r = 0, 0
        # 反向遍歷,尋找(t(這種情況
        for i in range(n-1, -1, -1):
            # 由於反向遍歷,所以非法的判斷條件和正向相反
            if s[i] == '(':
                l += 1
                if l > r:
                    l, r = 0, 0
                elif l == r:
                    ans = max(ans, l+r)
            else:
                r += 1
        return ans

這種方法實現非常簡單,幾乎毫無難度,效率也很高,是\(O(n)\)的演算法,但是需要對問題有很深的思考和理解才行。很多同學可能會苦惱,覺得這種方法太取巧了,自己不一定能想得到這麼巧妙的方法。沒有關係,我們接下來會繼續介紹一種中規中矩比較容易想到的方法。


dp


接下來要介紹的是鼎鼎大名的dp演算法,dp是英文dynamic programming的縮寫,翻譯過來的意思是動態規劃。它是一個頻繁出現在各個演算法領域以及面試當中的演算法,並且應用廣泛,在許多問題上用到了動態規劃的思路,可以說得上是教科書級的演算法了。因此對於我們演算法學習者來說,它也非常的重要。

很多初學者對於動態規劃可能理解並不深入,不是覺得非常困難,就是還停留在揹包問題的範疇當中。在這題當中,我會盡可能地講解清楚動態規劃的內在邏輯,以及它執行的原理,讓大家真正理解這一演算法的思路。至於動態規劃演算法具體的學習方法和一些經典例題,我們會放在之後的文章當中再詳細講解。所以如果是沒有基礎的同學,也不用擔心,接下來的內容也一樣能夠看懂。

動態規劃最樸素的思路就是拆分問題,將大問題拆分成小問題。但是和分治演算法不同的是,動態規劃更加關注子問題和原問題之間的邏輯聯絡,而分治演算法可能更加側重拆分。並且分治演算法的拆分通常是基於資料和問題規模的,而動態規劃則不然,更加側重邏輯上的聯絡。除此之外,動態規劃也非常注重模式的構造。

如果你看到這裡一臉懵逼,啥也沒看明白,沒有關係,我們用實際問題來舉例就明白了。我們先來學一個技巧,在動態規劃問題當中,我們最經常乾的一件事情就是建立一個叫做dp的陣列,它來記錄每一個位置能夠達到的最佳結果。比如在這題當中,最佳結果就是最長匹配的括號串。所以dp[i]就記錄以s[i]結尾的字串能夠構成的最長的匹配串的長度。

那麼,我們繼續分析,假設當前處理的位置i之前的結果都已經存在了,我們怎麼通過之前的資料獲得當前的dp[i]呢?這個可以認為是動態規劃的精髓,利用之前已經儲存的結果推算當前需要求的值。

顯然如果s[i]是(,沒什麼好說的,以i為結尾一定不能構成合法的串,那麼dp[i]=0。也就是說只有s[i]是)的時候,dp[i]的值才有可能大於0。那麼這個值會是多少呢?我們繼續來推算。

顯然,我們需要觀察i-1的位置,如果i-1的位置是(,那麼說明我們至少可以構成一個match。構成這個match之後呢?其實就要看dp[i-2]了。因為在一個合法的結果後面加上一個括號對顯然也是合法的。所以如果i-1處的結果是(,那麼我們可以得到dp[i] = dp[i-2] + 2。

那如果i-1的位置也是)呢?我們來舉個例子看看就知道了。

s:    a       b    (    )     )
idx:  i-4    i-3   i-2  i-1   i

從上面這個例子可以看出來,當i-1的位置也是)的時候,我們可以知道dp[i-1]有可能不為0,那麼很簡單,我們只需要跳過dp[i-1]長度的位置就好了。比如上面這個例子,i-1的位置可以和i-2構成match,那麼我們就可以跳過dp[i-1]也就是2個長度,去檢視i-3的位置和i是否構成match,如果構成match,那麼最終的答案就是dp[i-1] + 2 + dp[i-4]。因為dp[i-4]也有可能還有合法的串。

所以,到這裡我們就把所有子問題之間的邏輯聯絡都分析清楚了。剩下的就很簡單了,我們只需要根據上面的分析結果寫出答案而已。

不過還有一點,由於我們一直是利用前面的結果來推導後面的結果,我們需要一個初始的推導基點。這個基點就是dp[0],顯然在這個問題當中dp[0]=0。這個基點有了,剩下的就順理成章了。

我們寫出程式碼來看下:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        ans = 0
        dp = [0 for _ in range(n)]
        for i in range(1, n):
            if s[i] == ')':
                # 如果i-1是(,那麼我們判斷i-2
                if s[i-1] == '(':
                    dp[i] = 2 + (dp[i-2] if i > 1 else 0)
                # 如果i-1也是),我們需要繼續往前判斷
                # 這裡要特別注意下下標, 很容易寫錯
                elif i - dp[i-1] > 0 and s[i - dp[i-1] - 1] == '(':
                    dp[i] = 2 + dp[i-1] + (dp[i - dp[i-1] - 2] if i - dp[i-1] - 2 >= 0 else 0)
                    
            ans = max(ans, dp[i])
        return ans

我相信上面的解釋應該都能看懂,其實是很簡單的推理。我相信即使是對dp不太熟悉的同學,也應該都能看懂整個的執行原理。整個過程同樣是\(O(n)\)的計算過程,但是和上面的方法相比,我們額外開闢了陣列記錄每個位置的狀態。這也是dp演算法的特點,就是我們會儲存幾乎所有中間狀態的結果,因為我們邏輯關係上的推導過程正是基於這些中間狀態進行的。

所以這題雖然是Hard,但如果從dp的角度來講,如果你能想到用dp演算法來解決,其實距離解開真的已經不遠了。所以不要被題目上標記的Hard嚇到,真的沒有那麼難。另外,我個人也覺得這題將演算法的魅力發揮得非常明顯,尤其是第二種解法真的非常巧妙。希望大家都能喜歡這題。

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

相關推薦

LeetCode 32並不Hard難題解法超級經典領略動態規劃精彩

本文始發於個人公眾號:TechFlow,原創不易,求個關注 今天給大家分享的是LeetCode當中的32題,這是一道Hard難度的題。也是一道經典的字串處理問題,在接下來的文章當中,我們會詳細地解讀有關它的三個解法。 希望大家不要被題目上的標記嚇到,雖然這題標著難度是Hard,但其實真的不難。我自信你們看完

蘇蘇醬陪動態規劃拿名企offer

        轉眼間我已經是一名研三的老學長,時間飛快,感謝網際網路知識社群在我成長中給我的幫助。沒有網際網路社群的知識共享,很多知識我也難以快速瞭解;沒有網際網路社群的知識共享,我也不會發現原來很多看似絞盡腦汁不得解的難題,其實似乎也有章法可循。 &n

為什麼要學習C++它到底能做什麼?小編揭祕~

C++是由貝爾實驗室的Bjarne Strou-strup在C的基礎上推出的,它進一步擴充和完善了C語言,既可以進行C語言的過程化程式設計,又可以進行以抽象資料型別為特點的基於物件的程式設計,還可以進行以繼承和多型為特點的面向物件的程式設計。下面就讓我們一起來領略C++之美吧. (小編推薦

微酋長領略小程式大事記年史

微信小程式大事記編年史 12月21日:微信小程式開放新增功能:分享,自定義模板訊息,客服訊息,掃一掃; 12月30日:微信開放帶引數二維碼,允許已上線的小程式製作進入任意頁面的二維碼; 1月9日:微信開放微信小程式; 1月22日:微信增加社交分類,允許提交社交類

Python網路爬蟲入門領略Python爬蟲的樂趣!

前段時間小編寫了一篇有關於Python入門的文章,我覺得寫的還是不夠好,所以我特地補上一篇Python爬蟲的入門的,本文特別適合Python小白,剛學習爬蟲不久。接下來就讓我們一起來寫第一個例子吧!

GIAC現場Mob領略“智慧化+人性化”的未來開發場景

2018年11月23日,GIAC全球網際網路架構大會在上海隆重召開。作為中國網際網路技術領域一年一度的行業盛事,本屆大會從系統架構設計、機器學習、程式語言、分散式架構等領域,甄選前沿科技企業代表分享2018年度最值得的回顧、總結的創新技術及研發實踐案例,實踐啟示,共同探索未來的網際網路構建方式。

詩詞歌賦樣樣精通!詩詞古語小程式領略魅力古風丨實戰

1. 小程式功能 古詩詞大全 成語大全 成語接龍 詩詞飛花令 詩詞分享、收藏 詩詞接龍 唐詩宋詞起名字 百家姓 猜謎語 2. 小程式預覽: 3. 部分截圖 首頁 列表頁 詳情頁 分享頁 唐詩宋詞 成語接龍 4. 專案結構 . ├── README.md ├── project.

看完就懂五千字長文領略推薦系統

最近有一些小夥伴給我留言說非常想要我開一個推薦系統專題,其實我也有過這個想法,一直沒動筆主要有兩個原因。第一個原因是擔心自己水平不夠,班門弄斧或者是誤導了一些讀者。第二個原因是,我的確不知道這個專題應該怎麼寫。但是讀者有求,總得迴應不是,所以咬著牙寫了本文。 文章有點長,但是乾貨不少,希望大家能夠耐心讀完。

領略拼多多2020校招筆試題這樣的難度可以搞定嗎?

大家好,本來今天想寫一篇演算法和資料結構的。但是看了一眼計劃,發現基本上大部分基礎的內容都已經講過了。接下去就是一些競賽相關的演算法了,剛好最近是校招季,所以寫一點筆試題的題解,也許對大家的招聘有點用。 這一次選了拼多多的校招筆試題其中的一題,在寫文章的時候還看到了小馬智行的。也就是那個樓教主創辦的著名的po

leetcode 746.爬樓梯的最小代價(從暴力遞迴到動態規劃

題目: On a staircase, the i-th step has some non-negative cost cost[i] assigned (0 indexed). Once you pay the cost, you can either cl

LeetCode刷題Easy篇斐波那契數列問題(遞迴,尾遞迴非遞迴和動態規劃解法

題目 斐波那契數列:  f(n)=f(n-1)+f(n-2)(n>2) f(0)=1;f(1)=1;  即有名的兔子繁衍問題  1 1 2 3 5 8 13 21 .... 我的解法 遞迴 public static int Recursion

Leetcode 32. Longest Valid Parentheses--求最大連續的成對圓括號的長度如果中間有多餘的單個圓括號則不算作連續

Given a string containing just the characters '(' and ')', find the length of the longest valid (well-formed) parentheses substring.

LeetCode】120. Triangle 基於C++和Java的分析及解法動態規劃

120. Triangle Total Accepted: 69567Total Submissions: 229977Difficulty: Medium Given a triangle, find the minimum path sum from top t

table中的單行省略並不能讓td能自適應

targe href csdn mage cati 技術分享 ges details image 所以我不得對單行勝率設置width,多行在待測中(傳送門)。。。 table中的單行省略,並不能讓td能自適應

優於別人並不高貴真正的高貴是優於過去的自己

一個 alt img log ges 分享 過去的 進度 blog 更新一下今天的學習進度:以後每天都會更新,倘若有啥感悟想說的話也會一起發出來,希望更多的人能和我一起堅持下去:   1.每天背誦50個英文單詞,復習鞏固了60個單詞,進度: 750/3486  

全能自定義環境一鍵快速安裝PHP7.2版本32/64位任選

normal phpwamp 下載地址 ott 相對 新版 round -o href 想要在windows環境下快速搭建最新的PHP版本,可以使用全能自定義PHP集成環境PHPWAMP_IN2全能自定義:PHPWAMP_IN2支持一鍵自定義Apache、nginx、PHP

Leetcode 91. Decode Ways 解碼方法(動態規劃字符串處理)

範圍 解碼 length emp 添加 substr temp 字母 decode Leetcode 91. Decode Ways 解碼方法(動態規劃,字符串處理) 題目描述 一條報文包含字母A-Z,使用下面的字母-數字映射進行解碼 'A' ->

瑞薩真岡朋光:中國已向技術創新國轉變,日本需有清楚認識(向客戶提供的眾多優秀的解決方案並不會輕易就被取代)

子公司 供應商 人民幣 上海 努力 改革 集成電路 中國政府 投資 原標題:瑞薩電子真岡朋光:中國已向技術創新國轉變,日本需有清楚認識 作為老牌日本半導體產業的龍頭代表之一,瑞薩電子在全球微控制器(MCU)市場,特別是全球汽車市場長期占據著領先地位。然而近幾年來隨著日系半

javascript中的call.apply方法是針對function本身定義的內容並不能將

ons con ack min span 文章 apply call 發現 最近研究了js的繼承,看了幻天芒的文章http://www.cnblogs.com/humin/p/4556820.html#3947420,明白了最好是使用apply或call方法來實現繼承。 但

P問題NP問題NPC問題NP-hard問題

解決 找不到 存在 驗證 精確 font size n) 不能 1.P問題:一個問題能找到一個在多項式時間裏解決他的算法 多項式時間(o(1),o(lgn),o(n的a次方)) 非多項式時間 o(a的n次方) o(n!) 2.NP問題:在多項式時間找不到問題