字符串系列之最长回文子串
问题描述:
给定一个字符串S=A1A2...An,要求找出其最长回文子串(Longest Palindromic Substring)。所谓回文子串就是S的某个子串Ai...Aj为回文。例如,对字符串S=abcdcbeba,它的回文子串有:bcdcb,cdc,beb,满足题目要求的最长回文子串为bcdcb。
推理思路:
1.由于回文可能由奇数个字符组成,也可能由偶数个字符组成。对奇数回文的处理比较直观,只需要以某个字符为中心,依次向两边扩展即可。因此,我们可以通过如下方式把对偶数回文的处理转换成对奇数回文的处理:在字符边界添加特殊符号。例如,对字符串aba,预处理后变成#a#b#a#;对字符串abba,预处理后变成#a#b#b#a。可以看出,不管是奇数回文,还是偶数回文,在与处理后都变成奇数回文。在找出与预处理后字符串的最长回文后,只需要去除所有的#即为源字符串的最长回文。
2.对寻找字符串某类子串的问题,最简单直观的想法就是穷举出所有子串一一进行判别。这里也不例外,当然时间复杂度也很高,为O(n^3)。
3.对该问题,我们可以进行一定程度的简化处理。既然回文是一种特殊的字符串,我们可以以源字符串的每个字符为中心,依次寻找出最长回文子串P0, P1,...,Pn。这些最长回文子串中的最长串Pi = max(P1, P2,...,Pn)即为所求。请看源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 string find_lps_native(const string &str) 2 { 3 int center = 0, max_len = 0; 4 for(int i = 1; i < str.length()-1; ++i) 5 { 6 int j = 1; 7 8 //以str[i]为中心,依次向两边扩展,寻找最长回文Pi 9 while(i+j < str.length() && i-j >= 0 && str[i+j] == str[i-j]) 10 ++j; 11 --j; 12 if(j > 1 && j > max_len) 13 { 14 center = i; 15 max_len = j; 16 } 17 } 18 19 return str.substr(center-max_len, (max_len << 1) + 1); 20 }
4.可以看出,上面做法的复杂度为O(n^2)。相比穷举字符串的做法,已经降低了一个量级的复杂度。但是仔细想想,上面的算法还有改进空间吗?当然有!而且改进后能够把复杂度降低到O(n)!这就是大名鼎鼎的 Manacher’s Algorithm。请看下文分析:
举例说明:对字符串S=abcdabc而言,最长回文子串是以d为中心,半径为3的子串。当我们采用上面的做法分别求出以S[1]=a, S[2]=b, S[3]=c, S[4]=d为中心的最长回文子串后,对S[5]=a,S[6]=b...还需要一一进行扩展求吗?答案是NO。因为我们已经找到以d为中心,半径为3的回文了,S[5]与S[3],S[6]与S[2]...,以S[4]为对称中心。因此,在以S[5],S[6]为中心扩展找回文串时,可以利用已经找到的S[3],S[2]的相关信息直接进行一定步长的偏移,这样就减少了比较的次数(回想一下KMP中next数组的思想)。优化的思想找到了,我们先看代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 string find_lps_advance(const string &str) 2 { 3 //find radius of all characters 4 vector<int> p(str.length(), 0); 5 int idx = 1, max = 0; 6 for(int i = 1; i < str.length()-1; ++i) 7 { 8 if(max > i) 9 { 10 p[i] = p[(idx << 1) - i] < (max - i) ? p[(idx << 1) - i]:(max - i); 11 } 12 while(str[i+p[i]+1] == str[i-p[i]-1]) 13 p[i] += 1; 14 if(i + p[i] > max) 15 { 16 idx = i; 17 max = i+p[i]; 18 } 19 } 20 21 // find the character which has max radius 22 int center = 0, radius = 0; 23 for(int i = 0; i < p.size(); ++i) 24 { 25 if(p[i] > radius) 26 { 27 center = i; 28 radius = p[i]; 29 } 30 } 31 32 return str.substr(center-radius, (radius << 1) + 1); 33 }
这里进行简单的解释:上述代码中有三个主要变量,它们代表的意义分别是:
p:以S[i]为中心的最长回文串的半径为p[i]。
idx:已经找出的最长回文子串的起始位置。
max:已经找出的最长回文子串的结束位置。
算法的主要思想是:先找出所有的p[i],最大的p[i]即为所求。在求p[j] (j>i)时,利用已经求出的p[i]减少比较次数。
代码中比较关键的一句是:
p[i] = p[(idx << 1) - i] < (max - i) ? p[(idx << 1) - i]:(max - i);
在求p[i]时,如果max>i,则表明已经求出的最长回文中包含了p[i],那么与p[i]关于idx对称的p[ (idx << 1) - i]的最长回文子串可以提供一定的信息。看了两幅图大概就明白什么意思了:
求二者的最小值是因为当前能够获取的信息都来自max的左侧,需要进一步比较,求出以S[i]为中心的最长回文串。
5.除了上述的几种做法外,还可以利用动态规划以及后缀树来进行求解。下次进行介绍。
结束行文之前,补充一句,对于字符串类的问题,建议多画一画,寻找其中的规律。
参考文献: