LeetCode刷題實戰5:判斷迴文子串

 

算法的重要性,我就很少說了吧,想去大廠,就必需要通過基礎知識和業務邏輯面試+算法面試。因此,爲了提升你們的算法能力,這個公衆號後續天天帶你們作一道算法題,題目就從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/

 

翻譯算法

 

給定一個字符串s,要求它當中的最長迴文子串。能夠假設s串的長度最大是1000。數組

 

 

樣例
  •  
Example 1:
Input: "babad"Output: "bab"Note: "aba" is also a valid answer.Example 2:
Input: "cbbd"Output: "bb"
分析

 

雖然LeetCode裏給這道題的難度是Medium,但實際上並不簡單,咱們經過本身思考很難想到最佳解法。咱們先把各類算法放在一邊,先從最簡單的方法開始。最簡單的方法固然是暴力枚舉,可是這道題和以前的字符串問題不一樣。咱們在暴力枚舉的時候,並不須要枚舉全部的起始位置,再判斷這個子串是否迴文。實際上咱們能夠利用迴文串兩邊相等的性質,直接枚舉迴文串的中心位置,若是兩邊相等就往兩邊延伸。這樣咱們最多須要枚舉n個迴文中心,每次枚舉最多遍歷n次。因此最終的複雜度是O(n²)。有經驗的同窗看到這個複雜度就能反應過來,這明顯不是最優解法。可是對於當前問題,暴力枚舉當然不是最佳解法,但其實也算得上是不錯了,並無咱們想的那麼糟糕,不信的話,咱們來看另外一個看起來高端不少的解法。動態規劃(DP)

 

這道題中利用迴文串的性質還有一個trick,對於一個字符串S,若是咱們對它進行翻轉,獲得S_,顯然它當中的迴文子串並不會發生變化。因此若是咱們對翻轉先後的兩個字符串求最長公共子序列的話,獲得的結果就是迴文子串。ide

 

算法導論當中對這個問題的講解是使用動態規劃算法,便是對於字符串S中全部的位置i和S_中全部的位置j,咱們用一個dp數組記錄下以i和j結尾的S和S_的子串可以組成的公共子序列的最大的結果。學習

 

顯然,對於i=0,j=0,dp[i][j] = 0(假設字符串下標從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])

咱們不難觀察出來,這種解法的複雜度一樣是。而且空間複雜度也是O(n),也就是說咱們費了這麼大勁,並無起到任何優化。因此從這個角度來看,暴力搜索並非這題當中很糟糕的解法。code

 

分析到了這裏,也差很少了,下面咱們直接進入正題,這題的最佳解法,O(n)時間內獲取最大回文子串的曼徹斯特算法。blog

 

曼切斯特算法迴文串除了咱們剛剛提到的性質以外,還有一個性質,就是它分奇偶。簡而言之,就是迴文串的長度能夠是奇數也能夠是偶數。若是是奇數的話,那麼迴文串的迴文中心就是一個字符,若是是偶數的話,它的迴文中心實際上是落在兩個字符中間。舉個例子:ABA和ABBA都是迴文串,前者是奇迴文,後者是偶迴文。這兩種狀況不一致,咱們想要一塊兒討論比較困難,爲了簡化問題,咱們須要作一個預處理,將全部的迴文串都變成奇迴文。怎麼作呢,其實很簡單,咱們在全部兩個字符當中都插入一個特殊字符#。好比:abba -> #a#b#b#a#這樣一來,迴文中心就變成中間的#了。咱們再來看本來是奇迴文的狀況:aba -> #a#b#a#迴文中心仍是在b上,依然仍是奇迴文。
預處理的代碼:
  •  
def preprocess(text):    new_str = '#'    for c in text:        new_str += c + '#'    return new_str

 

曼切斯特算法用到三個變量,分別是數組p,idx和mr。咱們接下來一個一個介紹。leetcode

 

首先是數組radis,它當中存在的是每一個位置能構成的最長迴文串的半徑。注意,這裏不是長度,是半徑。

 

咱們舉個例子:

 

 

  •  
字符串S     # a # b # b # a #radis      1 2 1 2 5 2 1 2 1
咱們先不去想這個radis數組應該怎麼求,咱們來看看它的性質。首先,i位置的迴文串的半徑是radis[i],那麼它的長度是多少?很簡單: radis[2] * 2- 1。那麼,這個串中去掉#以後剩下的長度是多少?也就是說預處理以前的長度是多少?答案是radis[i] - 1,推算也很簡單,總長度是radis[i] * 2 - 1,其中#比字母的數量多一個,因此原串的長度是(radis[i] * 2 - 1 - 1)/2 = radis[i] - 1。也就是說原串的長度和radis數組就算是掛鉤了。idx很好理解,它就是指的是數組當中的一個下標,最後是mr,它是most_right的縮寫。它記錄的是在當前位置i以前的迴文串所向右能延伸到的最遠的位置。聽起來有些拗口,咱們來看個例子:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

此時i小於mr,mr對應的迴文中心是id。那麼i在id的迴文範圍當中,對於i而言,咱們能夠獲取到它關於id的對稱位置:id * 2 - i,咱們令它等於i_。知道這個對稱的位置有什麼用呢?很簡單,咱們能夠快速的肯定radis[i]的下界。在遍歷到i的時候,咱們已經有了i_位置的結果。經過i_位置的結果,咱們能夠推算i位置的範圍。

 

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

 

爲何是這個結果呢?

 

咱們把狀況寫全,假設mr-i > radis[i_]。那麼i_位置的迴文串所有都落在id位置的迴文串裏。這個時候,咱們能夠肯定radis[i]=radis[i_]。爲何呢?

 

由於根據對稱原理,若是以i爲中心的迴文串更長的話,咱們假設它的長度是radis[i_]+1。會致使什麼後果呢?若是這個發生,那麼根據關於id的對稱性,這個字符串關於id的對稱位置也是迴文的。那麼radis[i_1]也應該是這麼多才對,這就構成了矛盾。若是你從文字描述看不明白的話,咱們來看下面這個例子:

 

 

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

 

 

 

在這個例子當中,mr-i=5,radis[i_]=2。因此mr - i > radis[i_]。若是radis[i]=3,那麼x的位置就應該等於id的位置,同理根據對稱性,x_的位置也應該等於id的位置。那麼radis[i_]也應該是3。這就和它等於2矛盾,因此這是不可能出現的,在mr距離足夠遠的狀況下,radis[i_]的值限制了i位置的可能性。咱們再來看另外一種狀況,若是mr - i < radis[i_]時會怎麼樣呢?在這種狀況下,因爲mr距離i太近,致使i對稱位置的半徑沒法在i位置展開。可是mr的右側可能還存在字符,這些字符能夠構成新的迴文嗎?
  •  
字符串S     XXXXXXXXSXXXXXXXXXXXXXXXradis        i_    id    i mr
也就是說S[mr+1]會和S[i*2-mr-1]的位置相同嗎?其實咱們能夠不用判斷就能夠知道答案,答案是不會。舉個例子:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

根據對稱性,若是mr+1的位置對於i能夠構成新的對稱。因爲radis[i_] > mr-i,也就是說對於i_位置而言,它的對稱範圍可以輻射到mr對稱點的左邊。咱們假設這個地方的字母是a,根據對稱性,咱們能夠得出mr+1的位置也應該是a。如此一來,這兩個a又能構成新的對稱,那麼id位置的半徑就能夠再拓展1,這就構成了矛盾。因此,這種狀況下,因爲mr-i的限制,使得radis[i]只能等於mr - i。那什麼狀況下i位置的半徑能夠繼續拓展呢?只有mr - i == radis[i_]的時候,id構成的迴文串的左側對於i_可能構不成新的迴文,可是右側卻存在這種可能性。舉個例子:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

在上圖這個例子當中,i_的位置的迴文串向左只能延伸到ml,由於ml-1的位置和關於i_對稱的位置不相等。對於mr的右側,它徹底能夠既和i點對稱,又不會影響raids[id]的正確性。這個時候,咱們就能夠經過循環繼續遍歷,拓展i位置的迴文串。

 

整個過程的分析雖然不少,也很複雜,可是寫成代碼卻並很少。

 

 

  •  
# 初始化idx, mr = 0, 0# 爲了防止超界,設置字符串從1開始for i in range(1, n):  # 經過對稱性直接計算radis[i]  radis[i] = 1 if mr < i else min(radis[2 * idx - i], mr - i)  # 只有radis[i_] = mr - i的時候才繼續往下判斷  if radis[2 * idx - i] != mr - i and mr > i:    continue  # 繼續往下判斷後面的位置  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

 

 

到這裏,曼切斯特算法就算是實現完了。雖然咱們用了這麼多篇幅去介紹它,但是真正寫出來,它只有幾行代碼而已。不得不說,實在是很是巧妙,第一次學習可能須要反覆思考,才能真正理解。

 

不過咱們還有一個問題沒有解決,爲何這樣一個兩重循環的算法會是 O(n)的複雜度呢?

 

想要理解這一點,須要咱們拋開全部的虛幻來直視本質。雖然咱們並不知道循環進行了多少次,可是有兩點能夠確定。經過這兩點,咱們就能夠抓到複雜度的本質。

 

第一點,mr是遞增的,只會變大,不會減少。

第二點,mr的範圍是0到n,每次mr增長的數量就是循環的次數。

 

因此即便咱們不知道mr變化了多少次,每次變化了多少,咱們依然能夠肯定,這是一個O(n)的算法。

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=