1. 程式人生 > >談談回文子串

談談回文子串

image 復雜 整體 ++ 暴力 技術 判斷 span bsp

引子

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數組了。
現在再約定一個值MaxRightMaxRight表示當前所遍歷到的所有回文子串中,最靠右的索引。

技術分享

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),這個稍微解釋下,雖然代碼裏面是兩重循環,由於內層的循環只對尚未遍歷的部分進行,因此對於每一個字符而言,只會進行訪問一次。

談談回文子串