回文串指给定的字符串,正着读和反着读都是一样的。如ADA,反过来还是ADA即为回文串。最长回文子串指查找一给定字符串中最长的回文串。
通常有以下4种解法。主要考虑的是时间复杂度。
1:穷举法
穷举所有的子串,找出是回文串的子串,统计出最长的一个。
求每一个子串时间复杂度O(N^2),判断子串是不是回文O(N),两者是相乘关系,所以时间复杂度为O(N^3)。
代码如下:
#include <iostream> #include <string> using namespace std; /*穷举法获取得到每个字符子串,然后从两段开始判断字符字串是否是回文串。 回文指正着读和反着度 结果都是一样的。时间复杂度为O(n^3)*/ bool checkPalindrome(const string &s, int i, int j){ for(int k = 0; k < (j-i+1)/2; k++){ // 遍历(j-i+1)/2次数 if(s[i+k] != s[j-k]) return false; } return true; } void longestPadEnum(const string &s){ int begin=0; int maxSize = 0; for(int i = 0; i < s.size(); i++){ for(int j = i+1; j < s.size(); j++){ if(checkPalindrome(s, i, j) && j-i > maxSize){ begin = i; maxSize = j-i; // 回文串的长度为j-i+1 } } } cout << s.substr(begin, maxSize+1) << endl; } int main(){ string s; while(cin >> s){ longestPadEnum(s); } return 0; }
2:中心扩展法
回文串都是从中心开始的,我们把字符串的每个字母当做中心,向两边扩展,这样找最长的回文串。时间复杂度就变为了O(N^2)。
但是对于每个字母当做中心进行扩展的情况,都要考虑此时的回文串是偶数还是奇数,然后找出最长的一个。
如:(1)像aba,这样的长度为奇数。对应着代码标注的1:奇数情况
(2)像abba这样长度为偶数的回文串。处理对应着标注的2:偶数情况
代码如下:
/*中心扩展法 以i为中心像两边扩展,此时就要考虑回文是奇数还是偶数了,比如bab和baab的计算, 有两种方法: 1:奇数和偶数都考虑 2:在每两个字符之间加一个#,头尾也可以加,这样回文一定变为了奇数 如#b#a#b#长度为7, #b#a#a#b#长度变为了9 时间复杂度为O(n^2)*/ int getPalindrome1(const string &s, int i){ int j = 0; int maxSize = 0; while(i-j >= 0 && i+j < s.size()){ // 1:奇数的情况 if(s[i-j] == s[i+j]) j++; else break; } if(2*j-1 > maxSize){ maxSize = 2*j-1; } j = 0; while(i-j >= 0 && i+j+1 < s.size()){ // 2:回文为偶数情况 只有一种能情况能符合 没有放掉任何情况 if(s[i-j] == s[i+j+1])j++; else break; } if(2*j > maxSize){ maxSize = 2*j; } return maxSize; } void longestPadExtend1(const string &s){ int maxSize = 0; int mid = 0; for(int i = 0; i < s.size(); i++){ int padLen = getPalindrome1(s, i); if(padLen > maxSize){ maxSize = padLen; mid = i; } } // 输出回文串 int begin = 0; if(maxSize % 2)begin = mid- maxSize/2; else begin = mid - maxSize/2 + 1; int end = mid + maxSize/2; cout << s.substr(begin, end+1) << endl; }
当然存在这另外一种解决回文串是奇数还是偶数的问题。此时可以在字符串的头尾及每两个字符之间加入新的字符#(假设字符#不在字符串中出现过)。此时以每个字符向两边扩展,我们会发现#向后面扩展的长度都为奇数,字符串中字符向后扩展的长度都为偶数(包括自己)。如果是#向后扩展了3个,如为b#a#a#c,此时回文串为2;如果是字符串中的字符向后扩展了4个,如为d#b#a#b#c,,则回文串为3;因此可指此时最大的回文串个数就是某点字符向后扩展的个数-1。这其实就是manacher算法中的第一个性质。
代码如下:
/*字符串加入#号以后,每个字符处的j-1值即为该点的最大回文数*/ int getPalindrome2(const string &s, int i){ int j = 0; int maxSize = 0; while(i-j >= 0 && i+j < s.size()){ if(s[i-j]==s[i+j])j++; else break; } if(j > maxSize) maxSize = j; return maxSize; } // 通过添加# 将所有的回文串都变为奇数 void longestPadExtend2(const string &s){ string str = "#"; for(int i = 0; i < s.size(); i++) { str += s[i]; str += "#"; } int maxSize = 0; int mid = 0; for(int i = 0; i < str.size(); i++){ int pLen = getPalindrome2(str, i); if(pLen > maxSize){ maxSize = pLen; mid = i; } } // 输出回文串 int begin = 1, end = 1; if(maxSize % 2){ // 为奇数是# begin = mid - maxSize + 1; end = mid + maxSize - 2; }else { begin = mid - maxSize +2; end = mid + maxSize - 2; } for(int j = begin; j <= end; j+=2) cout << str[j]; cout << endl; //cout << str << endl; }
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)空间
代码如下:
/*动态规划求解回文字串c[i,j]为1表示从小标i到j为回文字串,为0表示不为回文串 初始化c[i,i] = 1; if(c[i][i+1] == c[i][i]) c[i][i+1] = 1 */ #include <iostream> #include <string> using namespace std; #define MAXSIZE 100 void findDPLongestPad(string s){ int c[MAXSIZE][MAXSIZE]; memset(c, 0, sizeof(c)); int maxSize = 1; int begin = 0; for(int i = 0; i < s.size(); i++) // 初始化 { c[i][i] = 1; if(i+1 < s.size() && s[i] == s[i+1]){ c[i][i+1] = 1; maxSize = 2; begin = i; } } for(int len = 3; len <= s.size(); len++){ for(int i = 0; i <= s.size() - len; i++ ){ int j = i+len-1; if(s[i] == s[j] && c[i+1][j-1]){ c[i][j] = 1; maxSize = len; begin = i; } } } /*for(int i = 0; i < s.size()-1; i++){ // 动态规划迭代 for(int j = i+1; j < s.size(); j++) { if(s[i] == s[j]){ if(j-i > 1) // 排除c[i,i+1]的情况 c[i][j] = c[i+1][j-1]+2;} else c[i][j] = 0; if(maxSize < c[i][j]){ maxSize = c[i][j]; begin = i; } } }*/ for(int i = 0; i < s.size(); i++) { for(int j = 0; j < s.size(); j++) cout << c[i][j] << " "; cout << endl; } cout << s.substr(begin, begin+maxSize) << endl; } int main(){ string s; while(cin >> s){ findDPLongestPad(s); } return 0; }
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)。
代码:
if(mx > i) { p[i] = (p[2*id - i] < (mx - i) ? p[2*id - i] : (mx - i)); } else { p[i] = 1; }
当 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,然后再去匹配了。
代码如下:
#include <iostream> #include <string> #include <algorithm> using namespace std; void manacherPalindrome(const string &s){ string str = "$#"; for(int i = 0; i < s.size(); i++) { str += s[i]; str += "#"; } int *p = new int[str.size()]; memset(p, 0, sizeof(int)*str.size()); int mx = 0, id = 0; // id为当前p[id]最大的位置,mx为其向右扩展的边界 即mx = id+[id] for(int i = 1; i < str.size(); i++){ // 求出p[i], 时间复杂度为O(n) if(mx > i) p[i] = min(p[2*id - i], mx-i); else p[i] = 1; while(i+p[i] < str.size() && str[i-p[i]] == str[i+p[i]]) // 如果开头不加$ 则需要判断i-p[i]是否大于等于0 p[i]++; if(i+p[i] > mx){ mx = i + p[i]; id = i; } } // 输出回文串 // p[i] 为奇数对应# 偶数为字符 p[i]最大值即为最大回文串+1 int maxSize = 0; int mid = 0; for(int i = 0; i < str.size(); i++){ if(maxSize < p[i]){ maxSize = p[i]; mid = i; } } for(int k = mid - maxSize + 2; k < mid + maxSize; k=k+2){ cout << str[k]; } cout << endl; delete []p; } int main(){ string s; while(cin >> s){ manacherPalindrome(s); } return 0; }
此Manacher算法使用id、mx做配合,可以在每次循环中,直接对P[i]的快速赋值,从而在计算以i为中心的回文子串的过程中,不必每次都从1开始比较,减少了比较次数,最终使得求解最长回文子串的长度达到线性O(N)的时间复杂度。
Hiho1032 最长回文子串
地址:http://hihocoder.com/problemset/problem/1032
代码:
#include <memory.h> #include <iostream> #include <algorithm> #include <string> using namespace std; int findLongestPalindrome(const string &s){ string str = "$#"; for(int i = 0; i < s.size(); i++){ str += s[i]; str += "#"; } int *p = new int[str.size()]; memset(p, 0, sizeof(int)*str.size()); int id = 0, mx = 0; for(int i = 1; i < str.size(); i++){ if(mx > i) p[i] = min(p[2*id-i], mx-i); else p[i] = 1; while(i+p[i] < str.size() && str[i-p[i]]==str[i+p[i]]) p[i]++; if(i+p[i] > mx){ mx = i+ p[i]; id = i; } } int maxSize = 0; for(int i = 0; i < str.size(); i++){ if(maxSize < p[i]) maxSize = p[i]; } delete []p; return maxSize-1; } int main(){ int N; cin >> N; while(N--){ string s; cin >> s; cout << findLongestPalindrome(s) << endl; } return 0; } /* input: 3 abababa aaaabaa acacdas output: 7 5 3 */
参考文献:
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/
原文地址:http://blog.csdn.net/lu597203933/article/details/44180939