Python演算法實踐——最長迴文串
給定一個字串,要求在這個字串中找到符合迴文性質的最長子串。所謂迴文性是指諸如 “aba”,"ababa","abba"這類的字串,當然單個字元以及兩個相鄰相同字元也滿足迴文性質。
看到這個問題,最先想到的解決方法自然是暴力列舉,通過列舉字串所有字串的起點,逐一判斷滿足迴文性的子串,記錄長度並更新最長長度。顯然這種演算法的時間複雜度是很高的,最壞情況可以達到O(N*N)。所以呢,這裡提出一個優化的方案,通過列舉字串子串的中心而不是起點,向兩邊同時擴散,依然是逐一判斷子串的迴文性。這種優化演算法比之前的演算法在最壞的情況下(即只有一種字元的字串)效率會有很大程度的上升。
由上述的優化方案,我們知道了列舉中心要比列舉起點效率要好,然而這並不是最優的演算法。由於列舉中心的演算法同時影響的是中心兩邊的字元,所以我們可以通過列舉中心的左邊字元作為中心的子串的迴文性判斷列舉中心右邊的字元作為中心得子串的迴文性,這就是manacher演算法。
manacher演算法思想非常巧妙,首先遍歷字串,假設 i 為列舉中心,則 j (j<i) 為中心的最長迴文子串長度發f[j] 便已經求出,此時 j 的影響範圍便是[j-f[j]/2,j+f [j]] 。為了使左邊的字元 j 對列舉中心右邊的影響最大,需要使 j+f[j]/2 最大。找到滿足j+f[j]/2最大的 j 之後,若 i 在[j,j+f[j]/2]中,則分兩種情況:
1 . i 關於 j 對稱的字元i'的影響範圍完全包含在j的影響範圍內,則由於迴文性,i 的影響範圍大於等於i'的影響範圍,即f[i]>=f[i']
2. i 關於 j 對稱的字元i'的影響範圍不完全包含在j的影響範圍內,此時i的右側影響範圍大於等於[j-f[j]/2,i'],即i+f[i]/2>=i'-j+f[j]/2
由於對稱性,可得i+i" = 2*j。因此第一種情況下,f[i]>=f[2*j-i];第二種情況下,f[i]>=f[j]+2*j-2*i。
綜上1,2,可得f[i]>=min(f[2*j-i],f[j]+2*j-2*i)。由於i右邊存在未遍歷的字元,因此在此基礎上,繼續向兩邊擴充套件,直到找到最長的迴文子串。
若i依然在j+f[j]/2後面,則表示i沒有被前面的字元的影響,只能逐一的向兩邊擴充套件。
這個演算法由於只需遍歷一遍字串,擴充套件的次數也是有限的,所以時間複雜度可以達到O(N)。
下面是Pthon3的程式,為了檢測演算法的效率,依然提供最初的暴力列舉演算法作為最壞演算法的參照。
#求最長迴文串類
class LPS:
#初始化,需要提供一個字串
def __init__(self,string):
self.string = string
self.lens = len(self.string)
#暴力列舉:作為演算法效率參照
def brute_force(self):
maxcount = 0
for j in range(self.lens):
for k in range(j,self.lens):
count = 0
l,m = j,k
while m>=l:
if self.string[l]==self.string[m]:
l,m = l+1,m-1
else:
break
if m<l:
count = k-j+1
if count>maxcount :
maxcount = count
return maxcount
#優化版:列舉子串中心
def brute_force_opti(self):
maxcount = 0
if self.lens == 1: #只有一個字元直接返回1
return 1
for j in range(self.lens-1): #列舉中心
count,u = 1,j
#對於奇數子串,直接擴充套件
for k in range(1,j+1): #兩邊擴充套件
l,m = u+k,j-k
if (m>=0)&(l<self.lens):
if(self.string[l]==self.string[m]):
count += 2
else:
break
if count>maxcount : #更新迴文子串最長長度
maxcount = count
if self.string[j]==self.string[j+1]: #處理偶數子串,將兩個相鄰相同元素作為整體
u,count= j+1,2
for k in range(1,j+1): #兩邊擴充套件
l,m = u+k,j-k
if (m>=0)&(l<self.lens):
if(self.string[l]==self.string[m]):
count += 2
else:
break
if count>maxcount : #更新迴文子串最長長度
maxcount = count
return maxcount
#manacher演算法
def manacher(self):
s = '#'+'#'.join(self.string)+'#' #字串處理,用特殊字元隔離字串,方便處理偶數子串
lens = len(s)
f = [] #輔助列表:f[i]表示i作中心的最長迴文子串的長度
maxj = 0 #記錄對i右邊影響最大的字元位置j
maxl = 0 #記錄j影響範圍的右邊界
maxd = 0 #記錄最長的迴文子串長度
for i in range(lens): #遍歷字串
if maxl>i:
count = min(maxl-i,int(f[2*maxj-i]/2)+1)#這裡為了方便後續計算使用count,其表示當前字元到其影響範圍的右邊界的距離
else :
count = 1
while i-count>=0 and i+count<lens and s[i-count]==s[i+count]:#兩邊擴充套件
count +=1
if(i-1+count)>maxl: #更新影響範圍最大的字元j及其右邊界
maxl,maxj = i-1+count,i
f.append(count*2-1)
maxd = max(maxd,f[i]) #更新迴文子串最長長度
return int((maxd+1)/2)-1 #去除特殊字元
通過上面的程式,使用字串為長度1000的純‘a’字串作為樣例,經過測試:
暴力列舉:49.719844s
中心列舉:0.334019s
manacher:0.008000s
由此可見,長度為1000時,暴力列舉的耗時已經無法忍受了,而相比而言,中心列舉在效率上已經有很大幅度的提升,最優的manacher耗時則為更短。