最長回文子串

2021-06-23 09:05:42 字數 4554 閱讀 5836

**:

最長回文子串是最初我在網易筆試的時候遇見的,當時天真的把原字串s倒轉過來成為s『,以為這樣就將問題轉化成為了求s和s』的

最長公共子串

的問題,而這個問題是典型的dp問題,我也在前面的文章中介紹了3中解決這個問題的方法。但是非常可惜,後來才知道這個演算法是不完善的。那麼到底為什麼呢?聽我慢慢道來。

s=「c a b a」  那麼  s' = 「a b a c」, 這樣的情況下 s和 s『的最長公共子串是aba。沒有錯誤。

但是當 s=「abacdfgdcaba」, 那麼s』 = 「abacdgfdcaba」。 這樣s和s『的最長公共子串是abacd。很明顯abacd並不是s的最長回文子串,它甚至連回文都不是。

現在是不是都明白為什麼最長回文子串不能轉化成為最長公共子串問題了。當原串s中含有乙個非回文的串的反序串的時候,最長公共子串的解法就是不正確的。正如上乙個例子中s既含有abacd,又含有abacd的反串cdaba,並且abacd又不是回文,所以轉化成為最長公共子串的方法不能成功。除非每次我們求出乙個最長公共子串的時候,我們檢查一下這個子串是不是乙個回文,如果是,那這個子串就是原串s的最長回文子串;如果不是,那麼就去求下乙個次長公共子串,以此類推。

最長回文子串有很多方法,分別是1暴力法,2 動態規劃, 3 從中心擴充套件法,4 著名的manacher演算法。下面我將分別介紹幾種方法。

方法一 暴力法

遍歷字串s的每乙個子串,去判斷這個子串是不是回文,是回文的話看看長度是不是比最大的長度maxlength大。遍歷每乙個子串的方法要o(n2),判斷每乙個子串是不是回文的時間複雜度是o(n),所以暴利方法的總時間複雜度是o(n3)。

方法二 動態規劃 時間複雜度o(n2), 空間複雜度o(n2)

動態規劃就是暴力法的進化版本,我們沒有必要對每乙個子串都重新計算,看看它是不是回文。我們可以記錄一些我們需要的東西,就可以在o(1)的時間判斷出該子串是不是乙個回文。這樣就比暴力法節省了o(n)的時間複雜度哦,嘿嘿,其實優化很簡單吧。

p(i,j)為1時代表字串si到sj是乙個回文,為0時代表字串si到sj不是乙個回文。

p(i,j)= p(i+1,j-1)(如果s[i] = s[j])。這是動態規劃的狀態轉移方程。

p(i,i)= 1,p(i,i+1)= if(s[i]= s[i+1])

string

longestpalindromedp

(strings)

;for(inti =0

;i i++)

for(inti = 0; i < n-1; i++)

}

for(intlen = 3; len <= n; len++)

}

}

returns.substr(longestbegin, maxlen);

}

方法三 中心擴充套件法

這個演算法思想其實很簡單啊,時間複雜度為o(n2),空間複雜度僅為o(1)。就是對給定的字串s,分別以該字串s中的每乙個字元c為中心,向兩邊擴充套件,記錄下以字元c為中心的回文子串的長度。但是有一點需要注意的是,回文的情況可能是 a b a,也可能是 a b b a。

string

expandaroundcenter

(strings,

intc1

,intc2

)returns

.substr(l

+1,r

-l-1

);}

string

longestpalindrome******

(strings)

returnlongest;

}

方法四 傳說中的manacher演算法。時間複雜度o(n)

這個演算法有乙個很巧妙的地方,它把奇數的回文串和偶數的回文串統一起來考慮了。這一點一直是在做回文串問題中時比較煩的地方。這個演算法還有乙個很好的地方就是充分利用了字元匹配的特殊性,避免了大量不必要的重複匹配。

演算法大致過程是這樣。先在每兩個相鄰字元中間插入乙個分隔符,當然這個分隔符要在原串中沒有出現過。一般可以用『#』分隔。這樣就非常巧妙的將奇數長度回文串與偶數長度回文串統一起來考慮了(見下面的乙個例子,回文串長度全為奇數了),然後用乙個輔助陣列p記錄以每個字元為中心的最長回文串的資訊。p[id]記錄的是以字元str[id]為中心的最長回文串,當以str[id]為第乙個字元,這個最長回文串向右延伸了p[id]個字元。

原串:    w aa bwsw f d

新串:           #  w  

# a # a #

b  # w # s # w # 

f  # d #

輔助陣列p:1  2  1 2 3 2 1  2  1  2 1 4 1 2 1  2 1 2 1

這裡有乙個很好的性質,p[id]-1就是該回文子串在原串中的長度(包括『#』)。如果這裡不是特別清楚,可以自己拿出紙來畫一畫,自己體會體會。當然這裡可能每個人寫法不盡相同,不過我想大致思路應該是一樣的吧。

現在的關鍵問題就在於怎麼在o(n)時間複雜度內求出p陣列了。只要把這個p陣列求出來,最長回文子串就可以直接掃一遍得出來了。

那麼怎麼計算p[i]呢?該演算法增加兩個輔助變數(其實乙個就夠了,兩個更清晰)id和mx,其中id表示最大回文子串中心的位置,mx則為id+p[id],也就是最大回文子串的邊界。

然後可以得到乙個非常神奇的結論,這個演算法的關鍵點就在這裡了:如果mx > i,那麼

p[i] >= min(p[2 * id - i], mx - i)。就是這個串卡了我非常久。實際上如果把它寫得複雜一點,理解起來會簡單很多:

//記j = 2 * id - i,也就是說 j 是 i 關於 id 的對稱點。

if (mx - i > p[j]) 

p[i] = p[j];

else /* p[j] >= mx - i */

p[i] = mx - i; // p[i] >= mx - i,取最小值,之後再匹配更新。

當 mx - i > p[j] 的時候,以s[j]為中心的回文子串包含在以s[id]為中心的回文子串中,由於 i 和 j 對稱,以s[i]為中心的回文子串必然包含在以s[id]為中心的回文子串中,所以必有 p[i] = p[j]。

當 p[j] > mx - i 的時候,以s[j]為中心的回文子串不完全包含於以s[id]為中心的回文子串中,但是基於對稱性可知,也就是說以s[i]為中心的回文子串,其向右至少會擴張到mx的位置,也就是說 p[i] >= mx - i。至於mx之後的部分是否對稱,就只能老老實實去匹配了。

由於這個演算法是線性從前往後掃的。那麼當我們準備求p[i]的時候,i以前的p[j]我們是已經得到了的。我們用mx記在i之前的回文串中,延伸至最右端的位置。同時用id這個變數記下取得這個最優mx時的id值。(注:為了防止字元比較的時候越界,我在這個加了『#』的字串之前還加了另乙個特殊字元『$』,故我的新串下標是從1開始的)

#include

#include

using

namespace

std;

const

intn

=300010

;intn,

p[n];

chars[

n],str[n];

#define

_min(x

,y)((

x)y)?(

x):(y))

void

kp()}}

void

init

()n =n

*2+2

;s[n

]=0;

}int

main

()return0;

}

if( mx > i)

p[i]=min( p[2*id-i], mx-i);

就是當前面比較的最遠長度mx>i的時候,p[i]有乙個最小值。這個演算法的核心思想就在這裡,為什麼p陣列滿足這樣乙個性質呢?

(下面的部分為形式)

leetcode上也有這個題的詳細說明,不過是英文版本的。

最長回文子串 最長回文子串行

1.最長回文子串行 可以不連續 include include include include using namespace std 遞迴方法,求解最長回文子串行 intlps char str,int i,int j intmain include include include using n...

最長回文子串

描述 輸入乙個字串,求出其中最長的回文子串。子串的含義是 在原串連續出現的字串片段。回文的含義是 正著看和倒著看是相同的,如abba和abbebba。在判斷是要求忽略所有的標點和空格,且忽略大小寫,但輸出時按原樣輸出 首尾不要輸出多餘的字串 輸入字串長度大於等於1小於等於5000,且單獨佔一行 如果...

最長回文子串

輸入乙個字元,求出其中最長的回文子串。子串的含義是 在元串中連續出現的字串片段。回文的含義是 正看和倒看相同,如abba和yyxyy,在判斷時候應該忽略所有的空格和標點符號,且忽略大小寫,但輸出應該保持原樣,輸入的字元長度不超過5000,且佔據單獨一行,輸出最長的回文子串 如有多個,輸出,起始位置最...