<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          LeetCode刷題實(shí)戰(zhàn)5:判斷回文子串

          共 4505字,需瀏覽 10分鐘

           ·

          2020-08-11 20:45



          算法的重要性,我就不多說了吧,想去大廠,就必須要經(jīng)過基礎(chǔ)知識(shí)和業(yè)務(wù)邏輯面試+算法面試。所以,為了提高大家的算法能力,這個(gè)公眾號(hào)后續(xù)每天帶大家做一道算法題,題目就從LeetCode上面選 !

          今天和大家聊的問題叫做判斷回文子串,這道題很有意思,我們先來看題面:


          Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

          https://leetcode.com/problems/longest-palindromic-substring/


          翻譯


          給定一個(gè)字符串s,要求它當(dāng)中的最長回文子串。可以假設(shè)s串的長度最大是1000。



          樣例

          Example 1:
          Input: "babad"Output: "bab"Note: "aba" is also a valid answer.Example 2:
          Input: "cbbd"Output: "bb"

          分析


          雖然LeetCode里給這道題的難度是Medium,但實(shí)際上并不簡單,我們通過自己思考很難想到最佳解法。

          我們先把各種算法放在一邊,先從最簡單的方法開始。最簡單的方法當(dāng)然是暴力枚舉,但是這道題和之前的字符串問題不同。我們?cè)诒┝γ杜e的時(shí)候,并不需要枚舉所有的起始位置,再判斷這個(gè)子串是否回文。實(shí)際上我們可以利用回文串兩邊相等的性質(zhì),直接枚舉回文串的中心位置,如果兩邊相等就往兩邊延伸。這樣我們最多需要枚舉n個(gè)回文中心,每次枚舉最多遍歷n次。所以最終的復(fù)雜度是O(n2)

          有經(jīng)驗(yàn)的同學(xué)看到這個(gè)復(fù)雜度就能反應(yīng)過來,這明顯不是最優(yōu)解法。但是對(duì)于當(dāng)前問題,暴力枚舉固然不是最佳解法,但其實(shí)也算得上是不錯(cuò)了,并沒有我們想的那么糟糕,不信的話,我們來看另一個(gè)看起來高端很多的解法。

          動(dòng)態(tài)規(guī)劃(DP)


          這道題中利用回文串的性質(zhì)還有一個(gè)trick,對(duì)于一個(gè)字符串S,如果我們對(duì)它進(jìn)行翻轉(zhuǎn),得到S_,顯然它當(dāng)中的回文子串并不會(huì)發(fā)生變化。所以如果我們對(duì)翻轉(zhuǎn)前后的兩個(gè)字符串求最長公共子序列的話,得到的結(jié)果就是回文子串。


          算法導(dǎo)論當(dāng)中對(duì)這個(gè)問題的講解是使用動(dòng)態(tài)規(guī)劃算法,即是對(duì)于字符串S中所有的位置i和S_中所有的位置j,我們用一個(gè)dp數(shù)組記錄下以i和j結(jié)尾的S和S_的子串能夠組成的公共子序列的最大的結(jié)果。


          顯然,對(duì)于i=0,j=0,dp[i][j] = 0(假設(shè)字符串下標(biāo)從1開始)


          我們寫出DP的代碼:

          for i in range(1, n):  for j in range(1, m):    if S[i] == S_[j]:      dp[i][j] = dp[i-1][j-1] + 1    else:      dp[i][j] = max(dp[i-1][j], dp[i][j-1])

          我們不難觀察出來,這種解法的復(fù)雜度同樣是。并且空間復(fù)雜度也是O(n),也就是說我們費(fèi)了這么大勁,并沒有起到任何優(yōu)化。所以從這個(gè)角度來看,暴力搜索并不是這題當(dāng)中很糟糕的解法。


          分析到了這里,也差不多了,下面我們直接進(jìn)入正題,這題的最佳解法,O(n)時(shí)間內(nèi)獲取最大回文子串的曼徹斯特算法。



          曼切斯特算法

          回文串除了我們剛剛提到的性質(zhì)之外,還有一個(gè)性質(zhì),就是它分奇偶。簡而言之,就是回文串的長度可以是奇數(shù)也可以是偶數(shù)。如果是奇數(shù)的話,那么回文串的回文中心就是一個(gè)字符,如果是偶數(shù)的話,它的回文中心其實(shí)是落在兩個(gè)字符中間。

          舉個(gè)例子:

          ABA和ABBA都是回文串,前者是奇回文,后者是偶回文。

          這兩種情況不一致,我們想要一起討論比較困難,為了簡化問題,我們需要做一個(gè)預(yù)處理,將所有的回文串都變成奇回文。怎么做呢,其實(shí)很簡單,我們?cè)谒袃蓚€(gè)字符當(dāng)中都插入一個(gè)特殊字符#。

          比如:

          abba -> #a#b#b#a#

          這樣一來,回文中心就變成中間的#了。我們?cè)賮砜丛臼瞧婊匚牡那闆r:

          aba -> #a#b#a#

          回文中心還是在b上,依然還是奇回文。

          預(yù)處理的代碼:

          def preprocess(text):    new_str = '#'    for c in text:        new_str += c + '#'    return new_str


          曼切斯特算法用到三個(gè)變量,分別是數(shù)組p,idx和mr。我們接下來一個(gè)一個(gè)介紹。


          首先是數(shù)組radis,它當(dāng)中存在的是每個(gè)位置能構(gòu)成的最長回文串的半徑。注意,這里不是長度,是半徑。


          我們舉個(gè)例子:



          字符串S     # a # b # b # a #radis      1 2 1 2 5 2 1 2 1

          我們先不去想這個(gè)radis數(shù)組應(yīng)該怎么求,我們來看看它的性質(zhì)。

          首先,i位置的回文串的半徑是radis[i],那么它的長度是多少?很簡單: radis[2] * 2- 1。那么,這個(gè)串中去掉#之后剩下的長度是多少?也就是說預(yù)處理之前的長度是多少?

          答案是radis[i] - 1,推算也很簡單,總長度是radis[i] * 2 - 1,其中#比字母的數(shù)量多一個(gè),所以原串的長度是(radis[i] * 2 - 1 - 1)/2 = radis[i] - 1。

          也就是說原串的長度和radis數(shù)組就算是掛鉤了。

          idx很好理解,它就是指的是數(shù)組當(dāng)中的一個(gè)下標(biāo),最后是mr,它是most_right的縮寫。它記錄的是在當(dāng)前位置i之前的回文串所向右能延伸到的最遠(yuǎn)的位置。

          聽起來有些拗口,我們來看個(gè)例子:


          ?


          此時(shí)i小于mr,mr對(duì)應(yīng)的回文中心是id。那么i在id的回文范圍當(dāng)中,對(duì)于i而言,我們可以獲取到它關(guān)于id的對(duì)稱位置:id * 2 - i,我們令它等于i_。知道這個(gè)對(duì)稱的位置有什么用呢?很簡單,我們可以快速的確定radis[i]的下界。在遍歷到i的時(shí)候,我們已經(jīng)有了i_位置的結(jié)果。通過i_位置的結(jié)果,我們可以推算i位置的范圍。


          radis[i]? >= min(radis[i_], mr-i)


          為什么是這個(gè)結(jié)果呢?


          我們把情況寫全,假設(shè)mr-i > radis[i_]。那么i_位置的回文串全部都落在id位置的回文串里。這個(gè)時(shí)候,我們可以確定radis[i]=radis[i_]。為什么呢?


          因?yàn)楦鶕?jù)對(duì)稱原理,如果以i為中心的回文串更長的話,我們假設(shè)它的長度是radis[i_]+1。會(huì)導(dǎo)致什么后果呢?如果這個(gè)發(fā)生,那么根據(jù)關(guān)于id的對(duì)稱性,這個(gè)字符串關(guān)于id的對(duì)稱位置也是回文的。那么radis[i_1]也應(yīng)該是這么多才對(duì),這就構(gòu)成了矛盾。如果你從文字描述看不明白的話,我們來看下面這個(gè)例子:



          S:       c a b c b d b c b a cradis:     x_  i_  5   i   x




          在這個(gè)例子當(dāng)中,mr-i=5,radis[i_]=2。所以mr - i > radis[i_]。如果radis[i]=3,那么x的位置就應(yīng)該等于id的位置,同理根據(jù)對(duì)稱性,x_的位置也應(yīng)該等于id的位置。那么radis[i_]也應(yīng)該是3。這就和它等于2矛盾,所以這是不可能出現(xiàn)的,在mr距離足夠遠(yuǎn)的情況下,radis[i_]的值限制了i位置的可能性。

          我們?cè)賮砜戳硪环N情況,如果mr - i < radis[i_]時(shí)會(huì)怎么樣呢?

          在這種情況下,由于mr距離i太近,導(dǎo)致i對(duì)稱位置的半徑無法在i位置展開。但是mr的右側(cè)可能還存在字符,這些字符可以構(gòu)成新的回文嗎?

          字符串S     XXXXXXXXSXXXXXXXXXXXXXXXradis        i_    id    i mr

          也就是說S[mr+1]會(huì)和S[i*2-mr-1]的位置相同嗎?

          其實(shí)我們可以不用判斷就可以知道答案,答案是不會(huì)。

          舉個(gè)例子:




          根據(jù)對(duì)稱性,如果mr+1的位置對(duì)于i可以構(gòu)成新的對(duì)稱。由于radis[i_] > mr-i,也就是說對(duì)于i_位置而言,它的對(duì)稱范圍能夠輻射到mr對(duì)稱點(diǎn)的左邊。我們假設(shè)這個(gè)地方的字母是a,根據(jù)對(duì)稱性,我們可以得出mr+1的位置也應(yīng)該是a。如此一來,這兩個(gè)a又能構(gòu)成新的對(duì)稱,那么id位置的半徑就可以再拓展1,這就構(gòu)成了矛盾。所以,這種情況下,由于mr-i的限制,使得radis[i]只能等于mr - i。

          那什么情況下i位置的半徑可以繼續(xù)拓展呢?

          只有mr - i == radis[i_]的時(shí)候,id構(gòu)成的回文串的左側(cè)對(duì)于i_可能構(gòu)不成新的回文,但是右側(cè)卻存在這種可能性。舉個(gè)例子:



          在上圖這個(gè)例子當(dāng)中,i_的位置的回文串向左只能延伸到ml,因?yàn)閙l-1的位置和關(guān)于i_對(duì)稱的位置不相等。對(duì)于mr的右側(cè),它完全可以既和i點(diǎn)對(duì)稱,又不會(huì)影響raids[id]的正確性。這個(gè)時(shí)候,我們就可以通過循環(huán)繼續(xù)遍歷,拓展i位置的回文串。


          整個(gè)過程的分析雖然很多,也很復(fù)雜,但是寫成代碼卻并不多。



          # 初始化idx, mr = 0, 0# 為了防止超界,設(shè)置字符串從1開始for i in range(1, n):  # 通過對(duì)稱性直接計(jì)算radis[i]  radis[i] = 1 if mr < i else min(radis[2 * idx - i], mr - i)  # 只有radis[i_] = mr - i的時(shí)候才繼續(xù)往下判斷  if radis[2 * idx - i] != mr - i and mr > i:    continue  # 繼續(xù)往下判斷后面的位置  while s[radis[i] + i] == s[i - radis[i]]:    radis[i] += 1  # 更新idx和mr的位置  if radis[i] + i > mr:    mr = radis[i] + i    idx = i



          到這里,曼切斯特算法就算是實(shí)現(xiàn)完了。雖然我們用了這么多篇幅去介紹它,可是真正寫出來,它只有幾行代碼而已。不得不說,實(shí)在是非常巧妙,第一次學(xué)習(xí)可能需要反復(fù)思考,才能真正理解。


          不過我們還有一個(gè)問題沒有解決,為什么這樣一個(gè)兩重循環(huán)的算法會(huì)是 O(n)的復(fù)雜度呢?


          想要理解這一點(diǎn),需要我們拋開所有的虛幻來直視本質(zhì)。雖然我們并不知道循環(huán)進(jìn)行了多少次,但是有兩點(diǎn)可以肯定。通過這兩點(diǎn),我們就可以抓到復(fù)雜度的本質(zhì)。


          第一點(diǎn),mr是遞增的,只會(huì)變大,不會(huì)減小。

          第二點(diǎn),mr的范圍是0到n,每次mr增加的數(shù)量就是循環(huán)的次數(shù)。


          所以即使我們不知道m(xù)r變化了多少次,每次變化了多少,我們依然可以確定,這是一個(gè)O(n的算法。


          如果喜歡本文,請(qǐng)順手點(diǎn)個(gè)贊或者轉(zhuǎn)發(fā)吧。


          上期推文:


          LeetCode刷題實(shí)戰(zhàn)1:在數(shù)組上遍歷出花樣

          LeetCode刷題實(shí)戰(zhàn)2:用鏈表模擬加法

          LeetCode刷題實(shí)戰(zhàn)3:最長不重復(fù)子串

          LeetCode刷題實(shí)戰(zhàn)4:兩個(gè)正序數(shù)組的中位數(shù)


          瀏覽 74
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产性情网站在线看 | 热久久最新视频 | 欧美性爱中文字慕 | 性爱无码AV | 日皮免费看 |