一、引例
#1032 : 最长回文子串
时间限制:1000ms
单点时限:1000ms
内存限制:64MB
描述:
小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进。
这一天,他们遇到了一连串的字符串,于是小Hi就向小Ho提出了那个经典的问题:“小Ho,你能不能分别在这些字符串中找到它们每一个的最长回文子串呢?”
小Ho奇怪的问道:“什么叫做最长回文子串呢?”
小Hi回答道:“一个字符串中连续的一段就是这个字符串的子串,而回文串指的是12421这种从前往后读和从后往前读一模一样的字符串,所以最长回文子串的意思就是这个字符串中最长的身为回文串的子串啦~”
小Ho道:“原来如此!那么我该怎么得到这些字符串呢?我又应该怎么告诉你我所计算出的最长回文子串呢?
小Hi笑着说道:“这个很容易啦,你只需要写一个程序,先从标准输入读取一个整数N(N<=30),代表我给你的字符串的个数,然后接下来的就是我要给你的那N个字符串(字符串长度<=10^6)啦。而你要告诉我你的答案的话,只要将你计算出的最长回文子串的长度按照我给你的顺序依次输出到标准输出就可以了!你看这就是一个例子。”
二、Manacher算法
首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#。
下面以字符串12212321为例,经过上一步,变成了 S[] = "$#1#2#2#1#2#3#2#1#";
然后用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:
S # 1 # 2 # 2 # 1 # 2 # 3 # 2 # 1 #
P 1 2 1 2 5 2 1 4 1 2 1 6 1 2 1 2 1
P[3]=2也正好是原始串S中以2为中心的回文串的总长度,即回文串122的长度为3.
所以我们只需要求出数组P,然后求P中的最大值即为S中最长回文串的长度。
那么怎么计算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,取最小值,之后再匹配更新。
借助[图来理解]
三、测试代码如下:
1 /* 2 ****************************最长回文子串(Mancher)************************************* 3 ******************************by JA/C++ 2015-1-16**************************************** 4 */ 5 6 #include<iostream> 7 #include<string> 8 #include<algorithm> 9 10 using namespace std; 11 12 string preProcess(string s) { 13 int n = s.length(); 14 if (n == 0) return "^$"; 15 string ret = "^"; 16 for (int i = 0; i < n; i++) 17 ret += "#" + s.substr(i, 1); 18 19 ret += "#$"; 20 return ret; 21 } 22 23 int longestPalindrome(string s) { 24 string T = preProcess(s); 25 int n = T.length(); 26 int *P = new int[n]; 27 int C = 0, R = 0; 28 for (int i = 1; i < n - 1; i++) { 29 int i_mirror = 2 * C - i; // equals to i' = C - (i-C) 30 31 P[i] = (R > i) ? min(R - i, P[i_mirror]) : 0; 32 33 // Attempt to expand palindrome centered at i 34 while (T[i + 1 + P[i]] == T[i - 1 - P[i]]) 35 P[i]++; 36 37 // If palindrome centered at i expand past R, 38 // adjust center based on expanded palindrome. 39 if (i + P[i] > R) { 40 C = i; 41 R = i + P[i]; 42 } 43 } 44 45 // Find the maximum element in P. 46 int maxLen = 0; 47 //int centerIndex = 0; 48 for (int i = 1; i < n - 1; i++) { 49 if (P[i] > maxLen) { 50 maxLen = P[i]; 51 //centerIndex = i; 52 } 53 } 54 delete[] P; 55 56 //return s.substr((centerIndex - 1 - maxLen) / 2, maxLen); 57 return maxLen; 58 } 59 60 int main() 61 { 62 int n; 63 string s; 64 cin >> n; 65 while (n--) 66 { 67 cin >> s; 68 cout << longestPalindrome(s) << endl; 69 } 70 }
参考文献:
1.Beeder's Blog 《最长回文子串》
2.LeetCode 《Longest Palindromic Substring》
3.ddyyxx的程序员之路 《Manacher算法总结》