談談回文子串
引子
1. 先講個歪果仁的故事,在龐貝古城的廢墟中,有一座名為赫庫蘭尼姆的城市,在這個遺跡中人們發現一塊石碑,石碑上寫著一個非常有趣的拉丁串:sator arepo tenet opera rotas翻譯到中文大概意思是:一個叫做arepo的耕作者,他用力地把著車輪。
這樣排列一下,從上下左右讀都是一樣的,歪果仁挺會完的。
2. 讓我印象更深刻的是高中老師給我們講的一個故事,有一天宋代著名文學家蘇軾和他的妹妹蘇小妹正在蕩舟湖上,欣賞著風景,忽然有人呈上秦少遊捎來的一封書信。打開一看,原來是一首別出心裁的回文詩:
蘇小妹看罷微微一笑,立即看出其中的奧秘,讀出了這首疊字回文詩:
靜思伊久阻歸期,
憶別離時聞漏轉,
時聞漏轉靜思伊。
蘇小妹被丈夫的一片癡情深深感到動,心中蕩起無限相思之情。面對一望無際的西湖美景,便仿少遊詩體,也作了一首回環詩,遙寄遠方的親人:
采蓮人在綠楊津,
在綠楊津一闋新;
一闋新歌聲漱玉,
歌聲漱玉采蓮人。
蘇東坡在一旁深為小妹的過人才智暗暗高興,他也不甘寂寞,略加沈吟,便提筆寫了如下一首:
賞花歸去馬如飛,
去馬如飛酒力微;
酒力微醒時已暮,
醒時已暮賞花歸。
蘇氏兄妹也派人將他們的詩作送與秦少遊。
老師講完這個故事我就感覺古人寫詩都是開掛的,這些詩倒過來還是一首完整詩,都是疊字回文詩。
故事是好故事,可是本人不太會講故事,關於回文的趣事還有很多,想看故事的可以自己去找。
正文
問題:給你一個字符串長度為n,現在讓你求出這個字符串最長回文子串的長度。
解法一:
純暴力,找出這個字符串的所有子串,然後判斷每個子串是否是回文串,維護更新最大的長度即可。空間復雜度O(1),時間復雜度O(n^3)。
解法二:
解法一實在是太暴力了,換個思路暴力,長度為奇數的回文串以中間字符為對稱軸成軸對稱,長度為偶數的回文串以中間空隙為對稱軸成軸對稱。那麽我們不就可以枚舉對稱軸,同時比較左右兩邊的字符,直到左右兩邊出現的字符不同或者達到邊界。枚舉的過程中維護更新最大長度即可。空間復雜度O(1),時間復雜度O(n^2)。雖然也很暴力,但是比解法一比起來就好太多了。
解法三:
1. 先解決分奇偶的問題
為了避免分奇偶討論,我們可以對原字符串進行一些處理,在原字符串中插入一些字符:
abcba 轉化為 #a#b#c#b#a#
abccba 轉化為 #a#b#c#c#b#a#
進行上面的處理後整個字符串的長度肯定為奇數,而且,不改變原串的回文結構。要保證這點我們選擇插入的字符一定要是原串中不存在的。
2. 避免重復計算
我們先來看看解法二中哪裏就有重復計算了
a b a b a
0 1 2 3 4
以1為對稱軸時,我們已經遍歷了aba,當以2為對稱軸的時候其實又遍歷了一遍aba,左邊的子串aba被遍歷了兩次。其實遍歷過的部分只要提取出有用的部分保持下來就無需再遍歷了。
回文半徑:最左或最右位置的字符與其對稱軸的距離。
現在申請一個數組LR,LR[i]表示以i為對稱軸的回文半徑。
# | a | # | b | # | c | # | b | # | a | # | |
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
LR | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 |
LR-1 | 0 | 1 | 0 | 1 | 0 | 5 | 0 | 1 | 0 | 1 | 0 |
我們發現,max(LR-1)即時我們要求的結果。
現在問題就轉化為:怎麽快速的得到LR數組了。
現在再約定一個值MaxRight
,MaxRight
表示當前所遍歷到的所有回文子串中,最靠右的索引。
pos為對稱軸,從左往右地遍歷字符串來求RL,假設當前訪問到的位置為i
,即要求RL[i],在對應上圖,i
必然是在pos
大(pos和pos前的已經求解完成)。但是i和MaxRight的相對關系並不確定。
1. i在MaxRight的左邊
從圖上觀察,我們可以利用已知部分來初步確定下LR[i]的值,假設i關於pos的對稱點時j,現在我們來梳理一下已知量:LR[j],MaxRight,pos,其中j = 2*pos - i.
假設LR[j]比較短,整體就包含在紅色區間內,那麽由於對稱性LR[i]≥LR[j].
現在RL[j]不完全包含在紅色區間內,上面JL等於JR關於j對稱,IL等於JR、JL等於IR關於pos對稱,那麽IL等於IR,也就是說LR[i]≥MaxRight-i.
通過上面兩種情況的討論,求解LR[i]的使用利用了已知部分的信息,避免了重復遍歷,我們可以得到LR[i] ≥ min(LR[j], MaxRight-i]),這個就很強。
對於後面還不確定的長度繼續遍歷即可,此時遍歷的都是之前沒有遍歷過的。
2. i在MaxRight的右邊
這種無需利用已知信息(也用不上),直接遍歷未知部分,更新MaxRight和pos即可。
一句話總結一下,上面我們在幹嘛,怎麽就優化了復雜度:維護MaxRight和pos,在更新RL[i]時避免了重復計算。
code:
1 const int MAXN = 1000010; 2 char Ma[2*MAXN]; // 插入字符後的原數組 3 int LR[2*MAXN]; // LR 4 int MR = 0; // MaxRight 5 int pos = 0; // pos 6 7 void Manacher(char s[], int len) 8 { 9 int l = 0; 10 11 // 處理原數組 12 Ma[l++] = ‘S‘; 13 Ma[l++] = ‘#‘; 14 for (int i = 0; i < len; ++i) { 15 Ma[l++] = s[i]; 16 Ma[l++] = ‘#‘; 17 } 18 Ma[l] = 0; 19 20 for (int i = 0; i < l; ++i) { 21 22 // 利用已知信息,避免重復計算 23 LR[i] = MR > i ? min(LR[2*pos-i], MR-i) : 1; 24 25 // 繼續找 26 while (Ma[i+LR[i]] == Ma[i-LR[i]]) { 27 ++LR[i]; 28 } 29 30 // 更新MaxRight pos 31 if (i + LR[i] > MR) { 32 MR = i +LR[i]; 33 pos = i; 34 } 35 } 36 }
算法空間復雜度O(n),時間復雜度O(n),這個稍微解釋下,雖然代碼裏面是兩重循環,由於內層的循環只對尚未遍歷的部分進行,因此對於每一個字符而言,只會進行訪問一次。
談談回文子串