最长回文子串

回文串指给定的字符串,正着读和反着读都是一样的。如ADA,反过来还是ADA即为回文串。最长回文子串指查找一给定字符串中最长的回文串。

通常有以下4种解法。主要考虑的是时间复杂度。

1:穷举法

穷举所有的子串,找出是回文串的子串,统计出最长的一个。

求每一个子串时间复杂度O(N^2),判断子串是不是回文O(N),两者是相乘关系,所以时间复杂度为O(N^3)。

代码如下:

 

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. #include <iostream>  
  2. #include <string>  
  3. using namespace std;  
  4.   
  5.   
  6. /*穷举法获取得到每个字符子串,然后从两段开始判断字符字串是否是回文串。 
  7. 回文指正着读和反着度 结果都是一样的。时间复杂度为O(n^3)*/  
  8. bool checkPalindrome(const string &s, int i, int j){  
  9.     for(int k = 0; k < (j-i+1)/2; k++){    // 遍历(j-i+1)/2次数  
  10.         if(s[i+k] != s[j-k]) return false;   
  11.     }  
  12.     return true;  
  13. }  
  14.   
  15. void longestPadEnum(const string &s){  
  16.   
  17.     int begin=0;  
  18.     int maxSize = 0;  
  19.     for(int i = 0; i < s.size(); i++){  
  20.         for(int j = i+1; j < s.size(); j++){  
  21.             if(checkPalindrome(s, i, j) && j-i > maxSize){  
  22.                 begin = i;  
  23.                 maxSize = j-i;           // 回文串的长度为j-i+1  
  24.             }  
  25.         }  
  26.     }  
  27.     cout << s.substr(begin, maxSize+1) << endl;  
  28. }  
  29.   
  30. int main(){  
  31.     string s;  
  32.     while(cin >> s){  
  33.         longestPadEnum(s);  
  34.     }  
  35.     return 0;  
  36. }  

2:中心扩展法

回文串都是从中心开始的,我们把字符串的每个字母当做中心,向两边扩展,这样找最长的回文串。时间复杂度就变为了O(N^2)。

但是对于每个字母当做中心进行扩展的情况,都要考虑此时的回文串是偶数还是奇数,然后找出最长的一个。

如:(1)像aba,这样的长度为奇数。对应着代码标注的1:奇数情况

(2)像abba这样长度为偶数的回文串。处理对应着标注的2:偶数情况

代码如下:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. /*中心扩展法 以i为中心像两边扩展,此时就要考虑回文是奇数还是偶数了,比如bab和baab的计算, 
  2. 有两种方法: 
  3. 1:奇数和偶数都考虑 
  4. 2:在每两个字符之间加一个#,头尾也可以加,这样回文一定变为了奇数 如#b#a#b#长度为7, #b#a#a#b#长度变为了9 
  5. 时间复杂度为O(n^2)*/  
  6. int getPalindrome1(const string &s, int i){  
  7.     int j = 0;  
  8.     int maxSize = 0;  
  9.     while(i-j >= 0 && i+j < s.size()){   // 1:奇数的情况  
  10.         if(s[i-j] == s[i+j]) j++;  
  11.         else break;  
  12.     }  
  13.     if(2*j-1 > maxSize){  
  14.         maxSize = 2*j-1;  
  15.     }  
  16.     j = 0;  
  17.     while(i-j >= 0 && i+j+1 < s.size()){   //  2:回文为偶数情况 只有一种能情况能符合 没有放掉任何情况  
  18.         if(s[i-j] == s[i+j+1])j++;  
  19.         else break;  
  20.     }  
  21.     if(2*j > maxSize){  
  22.         maxSize = 2*j;  
  23.     }  
  24.     return maxSize;  
  25. }  
  26.   
  27. void longestPadExtend1(const string &s){  
  28.   
  29.     int maxSize = 0;  
  30.     int mid = 0;  
  31.     for(int i = 0; i < s.size(); i++){  
  32.         int padLen = getPalindrome1(s, i);  
  33.         if(padLen > maxSize){  
  34.             maxSize = padLen;  
  35.             mid = i;  
  36.         }  
  37.     }  
  38.   
  39.     // 输出回文串  
  40.     int begin = 0;  
  41.     if(maxSize % 2)begin = mid- maxSize/2;  
  42.     else begin = mid - maxSize/2 + 1;  
  43.     int end = mid + maxSize/2;  
  44.     cout << s.substr(begin, end+1) << endl;  
  45. }  

当然存在这另外一种解决回文串是奇数还是偶数的问题。此时可以在字符串的头尾及每两个字符之间加入新的字符#(假设字符#不在字符串中出现过)。此时以每个字符向两边扩展,我们会发现#向后面扩展的长度都为奇数,字符串中字符向后扩展的长度都为偶数(包括自己)。如果是#向后扩展了3个,如为b#a#a#c,此时回文串为2;如果是字符串中的字符向后扩展了4个,如为d#b#a#b#c,,则回文串为3;因此可指此时最大的回文串个数就是某点字符向后扩展的个数-1。这其实就是manacher算法中的第一个性质。

代码如下:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. /*字符串加入#号以后,每个字符处的j-1值即为该点的最大回文数*/  
  2. int getPalindrome2(const string &s, int i){  
  3.     int j = 0;   
  4.     int maxSize = 0;  
  5.     while(i-j >= 0 && i+j < s.size()){  
  6.         if(s[i-j]==s[i+j])j++;  
  7.         else break;  
  8.     }  
  9.     if(j > maxSize)  
  10.         maxSize = j;  
  11.     return maxSize;  
  12. }  
  13.   
  14.   
  15. // 通过添加# 将所有的回文串都变为奇数  
  16. void longestPadExtend2(const string &s){  
  17.     string str = "#";  
  18.     for(int i = 0; i < s.size(); i++)  
  19.     {  
  20.         str += s[i];  
  21.         str += "#";  
  22.     }  
  23.     int maxSize = 0;  
  24.     int mid = 0;  
  25.     for(int i = 0; i < str.size(); i++){  
  26.         int pLen = getPalindrome2(str, i);  
  27.         if(pLen > maxSize){  
  28.             maxSize = pLen;  
  29.             mid = i;  
  30.         }  
  31.     }  
  32.     // 输出回文串  
  33.     int begin = 1, end = 1;  
  34.     if(maxSize % 2){    // 为奇数是#  
  35.         begin = mid - maxSize + 1;  
  36.         end = mid + maxSize - 2;  
  37.     }else {  
  38.         begin = mid - maxSize +2;  
  39.         end = mid + maxSize - 2;  
  40.     }  
  41.     for(int j = begin; j <= end; j+=2)  
  42.         cout << str[j];  
  43.     cout << endl;  
  44.   
  45.     //cout << str << endl;  
  46. }  

3:动态规划

我们用c[i][j]=1来表示字符串从i到j为回文串,用c[i][j]=0来表示字符串从i到j为非回文串。因此当c[i][j]为回文串,c[i+1][j-1]也必为回文串。此时j-i+1即为最长的回文子串。

依据动态规划的步骤:

初始化:c[i][i]=1; 如果s[i]==s[i+1],则c[i][i+1]=1.


时间复杂度:O(N^2),但是此时比中心扩展法需要额外的O(N^2)空间

代码如下:

 

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. /*动态规划求解回文字串c[i,j]为1表示从小标i到j为回文字串,为0表示不为回文串 
  2. 初始化c[i,i] = 1; if(c[i][i+1] == c[i][i]) c[i][i+1] = 1   
  3. */  
  4.   
  5. #include <iostream>  
  6. #include <string>  
  7. using namespace std;  
  8. #define MAXSIZE 100  
  9.   
  10. void findDPLongestPad(string s){  
  11.   
  12.     int c[MAXSIZE][MAXSIZE];  
  13.     memset(c, 0, sizeof(c));      
  14.   
  15.     int maxSize = 1;  
  16.     int begin = 0;  
  17.     for(int i = 0; i < s.size(); i++)   // 初始化  
  18.     {  
  19.         c[i][i] = 1;  
  20.         if(i+1 < s.size() && s[i] == s[i+1]){  
  21.             c[i][i+1] = 1;  
  22.             maxSize = 2;  
  23.             begin = i;  
  24.         }  
  25.     }  
  26.   
  27.     for(int len = 3; len <= s.size(); len++){  
  28.         for(int i = 0; i <= s.size() - len; i++ ){  
  29.             int j = i+len-1;  
  30.             if(s[i] == s[j] && c[i+1][j-1]){  
  31.                 c[i][j] = 1;  
  32.                 maxSize = len;  
  33.                 begin = i;  
  34.             }  
  35.         }  
  36.     }  
  37.   
  38.     /*for(int i = 0; i < s.size()-1; i++){    // 动态规划迭代 
  39.         for(int j = i+1; j < s.size(); j++) 
  40.         { 
  41.             if(s[i] == s[j]){ 
  42.                 if(j-i > 1)    // 排除c[i,i+1]的情况 
  43.                     c[i][j] = c[i+1][j-1]+2;} 
  44.             else c[i][j] = 0; 
  45.             if(maxSize < c[i][j]){ 
  46.                 maxSize = c[i][j]; 
  47.                 begin = i; 
  48.             }  
  49.         } 
  50.     }*/  
  51.   
  52.     for(int i = 0; i < s.size(); i++)  
  53.     {  
  54.         for(int j = 0; j < s.size(); j++)  
  55.             cout << c[i][j] << " ";  
  56.         cout << endl;  
  57.   
  58.     }  
  59.   
  60.     cout << s.substr(begin, begin+maxSize) << endl;  
  61. }  
  62.   
  63.   
  64. int main(){  
  65.     string s;  
  66.     while(cin >> s){  
  67.         findDPLongestPad(s);  
  68.     }  
  69.     return 0;  
  70. }  

4:Manacher算法—O(n)

(1)算法基本要点:首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#。

(2)例子:

下面以字符串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.s. 可以看出,P[i]-1正好是原字符串中回文串的总长度)——性质1

(3)计算P[i]

下面就是要计算p[i],该算法增加两个辅助变量id和mx,其中id表示最大回文子串中心的位置,mx则为id+P[id],也就是最大回文子串的边界。

 

这个算法的关键点就在这里了:如果mx > i,那么P[i] >=MIN(P[2 * id - i], mx - i)。

代码:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. if(mx > i)  
  2. {  
  3.       p[i] = (p[2*id - i] < (mx - i) ? p[2*id - i] : (mx - i));  
  4. }  
  5. else  
  6. {  
  7.        p[i] = 1;  
  8. }  

当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图。


当 P[j] > mx - i 的时候,以S[j]为中心的回文子串不完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能一个一个匹配了。


对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。

代码如下:

 

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. #include <iostream>  
  2. #include <string>  
  3. #include <algorithm>  
  4. using namespace std;  
  5.   
  6. void manacherPalindrome(const string &s){  
  7.     string str = "$#";  
  8.     for(int i = 0; i < s.size(); i++)  
  9.     {  
  10.         str += s[i];  
  11.         str += "#";  
  12.     }  
  13.     int *p = new int[str.size()];  
  14.     memset(p, 0, sizeof(int)*str.size());  
  15.       
  16.     int mx = 0, id = 0; // id为当前p[id]最大的位置,mx为其向右扩展的边界 即mx = id+[id]  
  17.       
  18.     for(int i  = 1; i < str.size(); i++){   //  求出p[i], 时间复杂度为O(n)  
  19.         if(mx > i) p[i] = min(p[2*id - i], mx-i);  
  20.         else p[i] = 1;  
  21.   
  22.         while(i+p[i] < str.size() && str[i-p[i]] == str[i+p[i]])  // 如果开头不加$ 则需要判断i-p[i]是否大于等于0  
  23.             p[i]++;  
  24.   
  25.         if(i+p[i] > mx){  
  26.             mx = i + p[i];  
  27.             id = i;  
  28.         }  
  29.     }  
  30.     // 输出回文串  
  31.     // p[i] 为奇数对应#  偶数为字符  p[i]最大值即为最大回文串+1  
  32.     int maxSize = 0;  
  33.     int mid = 0;  
  34.     for(int i = 0; i < str.size(); i++){  
  35.         if(maxSize < p[i]){  
  36.             maxSize = p[i];  
  37.             mid = i;  
  38.         }  
  39.     }  
  40.     for(int k = mid - maxSize + 2; k < mid + maxSize; k=k+2){  
  41.         cout << str[k];  
  42.     }  
  43.   
  44.     cout << endl;  
  45.   
  46.     delete []p;  
  47.   
  48.   
  49. }  
  50.   
  51. int main(){  
  52.     string s;  
  53.   
  54.     while(cin >> s){  
  55.         manacherPalindrome(s);  
  56.     }  
  57.     return 0;  
  58. }  

Manacher算法使用idmx做配合,可以在每次循环中,直接对P[i]的快速赋值,从而在计算以i为中心的回文子串的过程中,不必每次都从1开始比较,减少了比较次数,最终使得求解最长回文子串的长度达到线性O(N)的时间复杂度。

 // manacher算法,时间复杂度为O(N), 其中包含了4中拓扑结构,只有当i>=mx 或者p[2*id-i]=mx-i时需要扩展,而p[2*id-i]大于或者小于mx-i时不需要进行扩展
// 时间复杂度可以依据变量mx扩展的长度来计算,最多只能扩展到2N的位置(即加入#号后字符串的长度),故时间复杂度为O(N)

Hiho1032 最长回文子串

地址:http://hihocoder.com/problemset/problem/1032

代码:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. #include <memory.h>  
  2. #include <iostream>  
  3. #include <algorithm>  
  4. #include <string>  
  5. using namespace std;  
  6.   
  7. int findLongestPalindrome(const string &s){  
  8.     string str = "$#";  
  9.     for(int i = 0; i < s.size(); i++){  
  10.         str += s[i];  
  11.         str += "#";  
  12.     }  
  13.     int *p = new int[str.size()];  
  14.     memset(p, 0, sizeof(int)*str.size());  
  15.   
  16.     int id = 0, mx = 0;  
  17.     for(int i = 1; i < str.size(); i++){  
  18.         if(mx > i) p[i] = min(p[2*id-i], mx-i);  
  19.         else p[i] = 1;  
  20.         while(i+p[i] < str.size() && str[i-p[i]]==str[i+p[i]])  
  21.             p[i]++;  
  22.         if(i+p[i] > mx){  
  23.             mx = i+ p[i];  
  24.             id = i;  
  25.         }  
  26.     }  
  27.     int maxSize = 0;  
  28.     for(int i = 0; i < str.size(); i++){  
  29.         if(maxSize < p[i])  
  30.             maxSize = p[i];  
  31.     }  
  32.     delete []p;  
  33.     return maxSize-1;  
  34.   
  35. }  
  36.   
  37. int main(){  
  38.     int N;  
  39.     cin >> N;  
  40.     while(N--){  
  41.         string s;  
  42.         cin >> s;  
  43.         cout << findLongestPalindrome(s) << endl;  
  44.     }  
  45.     return 0;  
  46. }  
  47.   
  48. /* 
  49. input: 
  50. 3 
  51. abababa 
  52. aaaabaa 
  53. acacdas 
  54.  
  55. output: 
  56. 7 
  57. 5 
  58. 3 
  59. */  

参考文献:

1:http://www.cnblogs.com/en-heng/p/3973679.html最长回文子串

2:http://www.cnblogs.com/biyeymyhjob/archive/2012/10/04/2711527.htmlO(n)回文子串(Manacher)算法

3:https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/01.05.md

4:http://blog.csdn.net/kangroger/article/details/37742639

5:http://blog.163.com/zhaohai_1988/blog/static/2095100852012716105847112/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值