經典的DP題,沒事就多複習幾次。而且解法多元,甚至有O(N)解法,十分有趣。

題目

字串s,求s裡可以找到最長的回文子字串。
任一種答案即可,s = “babad”,可以為”bab”或是”aba”。

解法

一般來說應該比較容易想到DP解法。
dp(l,r)表示子字串的起點為l,終點為r,若成立回文則為true,否則false。
轉移方程式:dp(l,r) = s[l]等於s[r] 且 r-l為1或dp(l+1,r-1)為true。
base cases:l和r相同時,只有單一字元,肯定回文。或是l和r相鄰且相同,無法再縮,也是回文(已經併入轉移方程)。
計算dp時順便更新長度以及起始座標,最後以起始座標+長度取得子字串回傳。
time-space O(N^2),跑了5095ms。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        N=len(s)
        dp=[[False]*N for _ in range(N)]
        size=1
        start=0

        #init size 1 substring
        for i in range(N):
            dp[i][i]=True
            
        for r in range(N):
            for l in range(r):
                if s[l]==s[r] and (r-l==1 or dp[l+1][r-1]):
                    dp[l][r]=True
                    if r-l+1>size:
                        size=r-l+1
                        start=l
                        
        return s[start:start+size]

再來是雙指標,這種方法比較像人類的查找方法。
分別以字串中的每一個位置為中心點,試圖往左右擴展。
比較特別的是回文中心部分有可能有多個字元,如”..a..”或”..aa..”甚至是”..aaaaaaa..”,所以在確認中心點後,若右方字元與中心相同,可以持續向右擴展,保證整個中心部位被納入。
再來是同時擴展兩方,若l左邊以及r右邊還有字元且相同,同時移動一格。停止擴展後更新長度。
time O(N^2) space O(1),雖然時間複雜度相同,但是跑起來快很多,只要1065ms。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        N=len(s)
        start=0
        size=1

        for center in range(N):
            l=r=center
            #expand center part
            while r+1<N and s[r]==s[r+1]:
                r+=1
            #expand two sides
            while l>0 and r+1<N and s[l-1]==s[r+1]:
                l-=1
                r+=1
            #update longest size
            if r-l+1>size:
                size=r-l+1
                start=l
                
        return s[start:start+size]

再來是time O(N) space O(1)的Manacher演算法,學了兩三個小時才大致搞懂,執行時間212ms,非常噁心。
參考文章: https://leetcode.wang/leetCode-5-Longest-Palindromic-Substring.html
https://leetcode.com/problems/longest-palindromic-substring/discuss/3337/Manacher-algorithm-in-Python-O(n)

大致整理一下:
先將原字串s以分隔符號包起來,這裡採用’‘,然後頭尾再用另外兩種符號夾住,得到新字串T。如’aba’變成’^_a_b_a$’。
C代表上次處理回文子字串的中心,而R代表上次回文子字串的右邊界,陣列P[i]代表以T[i]為中心可以往左右擴展的長度;同時也代表以其為中心,可以在s得到的最長回文子字串長度

假設i到C的距離為n,則C再往左走n次會抵達對稱點j。在R大於i的情況下,P[i]的值會等於P[j],否則必須重新以i為中心點向左右擴張,停止擴張後依情況更新C和R。
最後遍歷P陣列,找出最大的長度並更新中心點。因為P[i]代表整個字串長度,所以P[i]/2等於半徑,子字串起點=(center-size)/2。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        #pretreatment
        T='^_'+'_'.join(list(s))+'_$'
        N=len(T)
        P=[0]*N
        C=R=0
        for i in range(1,N-1):
            j=2*C-i
            if R>i:
                P[i]=min(R-i,P[j])
        
            #expand from center
            while T[i+1+P[i]]==T[i-1-P[i]]:
                P[i]+=1
            
            #update r
            if i+P[i]>R:
                C=i
                R=i+P[i]
            
        #find longest
        size=center=0
        for i in range(1,N-1):
            if P[i]>size:
                size=P[i]
                center=i
        
        start=(center-size)//2
        return s[start:start+size]