Manacher Algorithm 最长回文子串
问题:求一个字符串的最长回文子串的长度
http://hihocoder.com/problemset/problem/1032
1.Brute Force
枚举子串(起点和终点),再判断子串是否回文串。时间复杂度O(n^3)
2.稍优的算法
枚举子串的中点,从中点向两侧扩展判断回文串。时间复杂度O(n^2)
3.Manacher算法
在前一种算法的基础上再加以改进,达到O(n)的时间复杂度
(1)处理偶数长度的子串
首先考虑一个问题:长度为奇数的字符串的中心是中间那个字符的位置,而长度为偶数的字符串的中心是中间两个字符串之间的“缝隙”。枚举偶数长度的回文子串的中点,似乎不好处理(如何能枚举一个“缝隙”的位置呢?)
既然这样,设法做一种预处理,在原字符串s的间隙插入特殊字符,例如'#'(包括首尾),得到新字符串t
例如:
s: abcba
t: #a#b#c#b#a#
这样,枚举到'#'就表示枚举到偶数长度的子串的中点了
(2)辅助变量
回文半径:回文串最右字符与回文中心字符的距离。以t[i]为中心的回文子串t[l,r],其回文半径为r-i+1
例如:aba的回文半径为2
辅助数组len:len[i]表示字符串t中以第i个字符为中心的回文子串的回文半径
可知len[i]-1就是该回文子串在原字符串s中的长度
证明:
首先,在字符串t中,该回文子串长度是2*len[i]-1,以#开头且以#结尾;不妨先“去掉”开头或结尾的一个#,剩下的部分长度是2*len[i]-2,其中一半是原串s中的字符,一半是#,所以该子串在原串s中的长度就是len[i]-1
辅助变量pos和maxRight:maxRight是当前已知的所有回文子串的右端点能延伸到的最大值,取得这个最大值的回文串的中点是pos
(3)计算len数组
从左往右依次计算len[i],设j是i关于pos的对称点,有(j+i)/2 = pos,j = 2*pos - i(j<i,故len[j]是已知的)
讨论i的位置与maxRight的相对关系:
当i<=maxRight:
考虑len[j],若len[j] <= maxRight-i+1,则len[i] = len[j];否则len[i]的最小值是maxRight-i+1(从maxRight+1开始向右还可能再延伸以t[i]为中心的回文串长度)
当i>maxRight:
此时len[i]无法如前所述地利用之前已经求得的信息了,故只能从t[i]像左右扩展寻找最长回文长度
(4)时间复杂度分析
maxRight只会一直向右推移,i>maxRight时,对于一个字符t[i],它影响maxRight,使maxRight向右推移,此时我们进行了回文串的左右匹配过程;但是当i<maxRight时,就能直接利用之前以求得的信息,[0,maxRight]范围内的字符都暂时不再需要参与匹配了。所以字符串t中的每一个字符(作为右端字符)只会参与一次匹配,故时间复杂度为O(n)。(考虑某字符可能先被作为某子串的右端字符参与匹配,之后又作为另一子串的左端字符参与匹配,那么每个字符也最多只参与2次匹配,这仍然是线性的)
4.代码实现
1 int Manacher(string &s) 2 { 3 string t="#"; 4 for(int i = 0; i < s.size(); ++i) 5 t += s[i], t += '#'; 6 7 int len[t.size()]; 8 int maxLen = 0; 9 int pos = 0, maxRight = 0; 10 for(int i = 0; i < t.size(); ++i) 11 { 12 len[i] = i<=maxRight? min(len[2*pos-i], maxRight-i+1) : 1; 13 while(i-len[i]>=0 && i+len[i]<t.size() && t[i-len[i]]==t[i+len[i]]) 14 ++len[i]; 15 if(i+len[i] > maxRight) 16 pos = i, maxRight = i+len[i]-1; 17 maxLen=max(maxLen, len[i]-1); 18 } 19 return maxLen; 20 }
PS:如果要求最长回文串,那么在过程中记录下在字符串t中的maxLen对应的idx和len[idx]即可。(最长回文子串是t[idx-len[idx]+1, idx+len[idx]-1]中不为'#'的子序列组成的字符串)