基本思想:对于字符串中每一个字符str[i],求出以它为中心的最大回文串的长度P[i],输出P[i]最大值对应的回文串
以字符串str="babcbabcbaccba"为例
Step 1 字符串预处理
在字符串两端以及每个字符间插入特殊字符#,然后在两端分别插入^$,构成新字符串 T="^#b#a#b#c#b#a#b#c#b#a#c#c#b#a#$"
插入#的目的是处理长度为偶数的回文串,例如"abccba",如果不插入字符,它的回文中心没有字符;插入^$的目的是省去边缘检测的步骤
Step 2 确定每个字符位置的回文串长度P[i]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
^ | # | b | # | a | # | b | # | c | # | b | # | a | # | b | # | c | # | b | # | a | # | c | # | c | # | b | # | a | # | $ |
0 | 0 | 1 | 0 | 3 | 0 | 1 | 0 | 7 | 0 | 1 | 0 | 9 | 0 | 1 | 0 | 5 | 0 | 1 | 0 | 1 | 0 | 1 | 2 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
最大P[i]为i=12时P[i] = 9,因此最大回文子串为abcbabcba
为了简化计算P[i]的步骤,我们利用回文字串的中心对称性,维护L、C、R三个变量,可以分为两种情况
1、i关于C的对称字符i'对应的回文串未超出L,即i'-L = R - i < P[i']
如上图,P[13]和P[11]关于C对称,且T[11]对应的回文字串在L右边,因此P[13]=P[11]=1
2、i关于C的对称字符i′对应的回文串超出L,即i′−L=R−i≥P[i′]
此时需要从逐个字符判断P[i]的值
-
确定基础长度和起始字符位置:
- 如果i>R,基础长度为Len=0,起始字符位置为Ind=i+1
- 如果i≤R,基础长度为Len=R−i,起始字符位置为Ind=R+1
-
从Ind开始逐个字符搜索
-
如果T[i]对应的回文字串超出R,即Len>R−i,更新C、L、R
GET-LEN(T, i, R) if(i > R) Len = 0 Ind = i + 1 else Len = R - i Ind = R + 1 while(T[Ind] == T[2 * i - Ind]) Len = Len + 1 Ind = Ind + 1 if Len > R - i C = i L = C - Len R = C + Len
如上图,P[15]和P[7]关于C对称,但是T[11]对应的回文字串超出LL,因此P[15]≠P[7]=7
- 由于i=15<20=R,基础长度Len = R - i = 5,起始位置Ind = R + 1 = 21
- 第一次比较,T[21]≠T[2∗i−Ind]=T[9],检查结束,P[i] = Len = 5
- Len=5=R−i,不需要更新C、L、R
Step 3 找出PP中的最大值MaxLenMaxLen以及其位置MaxIndMaxInd,输出最长回文串
最长回文子串为str.substr((MaxInd - 1 - MaxLen)/2, MaxLen)
C++实现:
string preProcess(string str) { int len = str.length(); if (len == 0) return "^$"; string res = "^"; for (int i = 0; i < len; i++) res += "#" + str.substr(i, 1); res += "#$"; return res; } string longestPalindrome(string s) { string T = preProcess(s); int len = T.length(); vector<int> P(len); int C = 0, L = 0, R = 0; int MaxLen = 0, MaxInd; for (int i = 0; i < len; i++) { if (2 * C - i < 0 || 2 * C - i - P[2 * C - i] <= L) { int temp = R - i < 0? 0: R - i; int ind = i > R? i + 1: R + 1; while (ind < T.length() && 2 * i - ind >= 0 && T[ind] == T[2 * i - ind]) { temp++; ind++; } P[i] = temp; if (temp > R - i) { C = i; L = C - temp; R = C + temp; } } else P[i] = P[2 * C - i]; if (P[i] > MaxLen) { MaxLen = P[i]; MaxInd = i; } } for (int i = 0; i < len; i++) cout << T[i]; cout << endl; for (int i = 0; i < len; i++) cout << P[i]; cout << endl; return s.substr((MaxInd - 1 - MaxLen) / 2, MaxLen); }
时间复杂度分析
预处理部分,共需要循环N次,每次为O(1),总时间耗费为O(N)
计算PP时
- 第一种情况时间耗费为O(1),第一种情况出现不会超过N次,即O(N);
- 第二种情况,由于C,R,L动态变化,每个字符被比较的次数不会超过一次,因此总的比较次数不超过N次,即O(N)
综上所述,Manacher's Algorithm的时间复杂度为O(N) + O(N) + O(N) = O(N)