動態規劃之馬拉車演算法 Python解法

2021-10-23 12:50:10 字數 4468 閱讀 7038

給定乙個字串,你的任務是計算這個字串中有多少個回文子串。

具有不同開始位置或結束位置的子串,即使是由相同的字元組成,也會被視作不同的子串。如"abc"有三個回文子串『a','b','c'.

示例 1:

輸入:"abc"

輸出:3

解釋:三個回文子串: "a", "b", "c"

示例 2:

輸入:"aaa"

輸出:6

解釋:6個回文子串: "a", "a", "a", "aa", "aa", "aaa"

對於這個問題,簡單的思路是列舉字串中的每個子串,然後判斷子串中回文串的個數。o(n^3)

本篇文章主要介紹我個人對馬拉車演算法實現思路的一些想法,原題解請看leetcode-647.回文子串

由於回文串有對稱性這一特點,每個回文串必然存在乙個對稱中心。我們可以對字串中每個字元或兩個字元之間的位置(若回文串長度為偶數)當作對稱中心進行列舉,例如s="abcba",當i=2時,判斷a[1]==a[3],a[0]==a[4].....如果左右兩邊字元相同時則繼續擴充套件,一旦不相同則更新最大擴充套件長度並退出當前迴圈,繼續列舉下乙個對稱中心。

假設字串的長度為 n,列舉每個對稱中心(長度為 1 和 0的對稱中心分別有 n 和 n-1 個),共遍歷2n-1次,時間複雜度為 o(n);每個對稱中心最多向外拓展n次,所以整體的時間複雜度是 o(n^2)。空間複雜度為o(1)。

**如下(直接抄leetcode了):

//遍歷2n-1次,就能夠得到所有可能的對稱中心

for (int i = 0; i < 2 * n - 1; i++)

}

還有沒有優化的空間呢?讓我們看看在中心拓展法**現的兩個問題,然後了解一下馬拉車演算法如何對這兩個問題進行處理。

為了更簡單直接地說明問題,我舉乙個極端的特例:字串s="aaaaaaa",並先對奇回文子串進行查詢。儘管我們在以第2位字元為對稱中心左右擴充套件時能夠發現左回文子串s1="aaa",並且以第四位字元為對稱中心左右擴充套件能夠發現了長回文串s,那麼之後的查詢中是否仍需要匹配後三位字串呢?設im為長回文串的對稱中心

其實,我們可以發現,若在乙個長回文串中發現了左回文子串,根據回文串的對稱性,那麼在一定區域內的右子串中必然也存在乙個回文串。因此,上述重複查詢過程是可以省去的。

馬拉車演算法的核心思路就在於,對每個長回文串的對稱中心向兩邊進行拓展,同時記下前面已經查詢過的回文子串半徑(原理是動態規劃的思想),其本質在於,如果能夠找到乙個長回文串,同時在它的對稱中心左區域中找到乙個回文子串,由於回文串的對稱性,其右區域中必然是同樣的回文子串。因此這個演算法能快速地找出最長的回文子串。當然也能用來求解回文子串的個數。

1.預處理(解決了奇偶回文串問題):

預處理**如下

s = '$' + input().replace('', '#') + '!'

#為了避免陣列越界,讓開頭和結尾的兩個字元一定不相等,迴圈就可以在這裡終止。

2.初始化(解決了重複查詢問題):

這裡有兩種情況:

當找到乙個長回文串時,取其對稱中心為im,對於其中的每個對稱中心i,我們可以求出關於im的對稱位置j=2*im-i

在這個長回文串範圍內,可以通過對稱性得到d[i]=d[j] 。

注意當d[j]>r-i時,通過d加速拓展的右子串超出了長回文串的範圍。然而大於右端點的回文串我們還沒有開始匹配,此時d[i] = r - i .比方說,當im=3時,有r=5;當i=5時,有j=1,並且前面已經算出d[j] = 1。此時d[j]>r-i,向右拓展超出了長回文串範圍,則d[i]應為i到右端點r的距離,即r-i。因此,當對稱中心i被包含在當前最大回文串內時,有

d[i]=min(d[2 * im - i],r - i) (核心:如果d[j]+i<=r,則d[i]=d[j],否則d[i]=r-i)

3.中心拓展

經過了上面的初始化,我們每次列舉對稱中心的時候就都能夠保證 s[i-d[i]]=s[i+d[i]],而現在,只要再滿足ts[i - d[i]-1]==ts[i + d[i]+1],則不斷向左右兩邊拓展半徑。

綜上所述,我們能夠寫出馬拉車演算法的核心**:

for i in range(2, len(s)-2):      

if(i<=r):#當對稱中心i被包含在當前最大回文串內時,拓展過程可以加速(核心)

d[i]=min(d[2 * im - i] , r - i )

while (s[i - d[i] - 1] == s[i + d[i] + 1]):#滿足條件,向左右拓展半徑

d[i] += 1

4.維護當前最大回文串的對稱中心im和r我們仍以字串"dadbdac"為例,求d時可以只看s[2:len-2):s$

#d#a

#d#b

#d#a

#c#!

i012

3456

78910

1112

1314

1516

d[i]10

3010

5010

101

其實在上述的核心思路中,在列舉每個回文子串的對稱中心時,也在不斷匹配最大回文串並更新im和r。利用已經算出來的狀態來更新d[i],這就是一種動態規劃的思想

對於乙個回文串,我們用d[i]記錄了其對稱中心到右端點的距離。以上面的例子來說明,當i=8時(屬於i>r的情況),計算出d[i]=5,則此時容易得到r=i+d[i]=13,同時更新im=8。對於i<=r的情況,如果i+d[i]>r,說明當前對稱中心i拓展出了回文串,於是此時需要更新對稱中心im為i,右端點r為i+d[i]。而對於i>r的情況,由於d[i]總是等於0,此時更新im=i,r=i

if (i + d[i] > r):

im, r = i, i + d[i]

順帶提一句,最開始我以為馬拉車這個演算法名字應該是根據manacher英譯而來,但是這個中心擴充套件的思路,加上通過d[i]記錄了最大回文串的半徑避免了重複匹配,光看這兩行**:

while (s[i - d[i] - 1] == s[i + d[i] + 1]) d[i]++;

if (i + d[i] > r)

真是有兩頭馬在拉著乙個對稱中心向兩邊跑的感覺,大概就像這樣:

然後不斷更新最大回文串的對稱中心im和 r,通過d[i] = min(d[2 * im - i], r - i)快速求解如何求解ans就不解釋啦,直接上**:

s = '$' + input().replace('', '#') + '!'

d,im,r,ans= [0]*(len(s)-2),0,0,0

for i in range(1, len(s)-2):

if (i <= r):#當對稱中心i被包含在當前最大回文串內時,拓展過程可以加速

d[i] = min(d[2 * im - i], r - i) #核心:如果d[j]+i<=r,則d[i]=d[j],否則d[i]=r-i

while (s[i - d[i] - 1] == s[i + d[i] + 1]):#滿足條件,向左右拓展半徑

d[i] += 1

if (i + d[i] > r):

im, r = i, i + d[i]

ans += (d[i] + 1) // 2

print(ans)

這個演算法總時間複雜度是o(n),空間複雜度o(n)。

另外,如果再用乙個變數ma記錄d[i]的最大值,稍微改一下最後兩行,就可以找出字串中的最長回文子串:

ma,ans = (d[i],ts[i-d[i]:i+d[i]]) if (d[i]>ma) else (ma,ans)

print(ans.replace('#',''))

馬拉車演算法

思路筆記 上述情況1和情況2又可以歸結為 i 的回文半徑 和 r i的距離 中小的那個就是i的回文半徑。include include includeusing namespace std string manacherstring string str return res int min int...

馬拉車演算法

馬拉車演算法是一種計算最長回文子串的演算法,以其優秀的線性複雜度聞名於世,相較於o n 2 o n 2 o n2 的dpdp dp演算法和會被特殊資料卡到o n 2 o n 2 o n2 的暴力演算法,馬拉車演算法無疑是求解最長回文子串的最優選擇。最長回文子串分為偶數串和奇數串,為了避免這些問題,馬...

馬拉車演算法

manacher char s maxn 1 int n,hw maxn 1 int l maxn 1 r maxn 1 void manacher char a n len 2 2 s n 0 int maxr 0,m 0 for int i 1 i n i manacher 題意在給定的字串中找...