给一个字符串,写一个函数返回该字符串的最长回文子串的长度,无回文子串则返回0。
参考如下文章:
http://www.cnblogs.com/houkai/p/3371807.html
回文串就是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。回文子串,顾名思义,即字符串中满足回文性质的子串。比如输入字符串 "google”,由于该字符串里最长的对称子字符串是 "goog”,因此输出4。
1.问题解决的基本方法
分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。
要判断一个字符串是不是对称的,不是一件很难的事情。我们可以先得到字符串首尾两个字符,判断是不是相等。如果不相等,那该字符串肯定不是对称的。否则我们接着判断里面的两个字符是不是相等,以此类推。
- <pre name="code" class="cpp">/*
- *作者:侯凯
- *说明:求最长回文字符串
- *日期:2013-10-15
- */
- #include<iostream>
- using namespace std;
- //字符串是否对称
- bool isAym(char *cbegin, char *cend)
- {
- if(cbegin == NULL || cend ==NULL || cbegin > cend)
- {
- return false;
- }
- while(cbegin<cend)
- {
- if(*cbegin!=*cend)
- {
- return false;
- }
- cbegin++;
- cend--;
- }
- return true;
- }
<pre name="code" class="cpp">/*
*作者:侯凯
*说明:求最长回文字符串
*日期:2013-10-15
*/
#include<iostream>
using namespace std;
//字符串是否对称
bool isAym(char *cbegin, char *cend)
{
if(cbegin == NULL || cend ==NULL || cbegin > cend)
{
return false;
}
while(cbegin<cend)
{
if(*cbegin!=*cend)
{
return false;
}
cbegin++;
cend--;
}
return true;
}
现在我们试着来得到对称子字符串的最大长度。最直观的做法就是得到输入字符串的所有子字符串,并逐个判断是不是对称的。如果一个子字符串是对称的,我们就得到它的长度,最后经过比较,就能得到最长的对称子字符串的长度了。
- //O(n*n*n)复杂度的子字符串
- int getMaxSym(char * str)
- {
- if(str == NULL)
- return 0;
- int maxlength = 0, strlength = 0;
- char *pFirst = str;
- char *strEnd = str + strlen(str);
- while(pFirst < strEnd)
- {
- char *pLast = strEnd;
- while(pLast > pFirst)
- {
- if(isAym(pFirst, pLast))
- {
- strlength = pLast - pFirst + 1;
- if(strlength > maxlength)
- {
- maxlength = strlength;
- }
- }
- pLast --;
- }
- pFirst ++;
- }
- return maxlength;
- }
//O(n*n*n)复杂度的子字符串
int getMaxSym(char * str)
{
if(str == NULL)
return 0;
int maxlength = 0, strlength = 0;
char *pFirst = str;
char *strEnd = str + strlen(str);
while(pFirst < strEnd)
{
char *pLast = strEnd;
while(pLast > pFirst)
{
if(isAym(pFirst, pLast))
{
strlength = pLast - pFirst + 1;
if(strlength > maxlength)
{
maxlength = strlength;
}
}
pLast --;
}
pFirst ++;
}
return maxlength;
}
上述方法的时间效率:由于需要两重while循环,每重循环需要O(n)的时间。另外,我们在循环中调用了IsSym,每次调用也需要O(n)的时间。因此整个函数的时间效率是O(n^3)。
假设输入:abcddcba,按照上述程序,要分割成 'abcddcba’, 'bcddcb’, 'cddc’, 'dd’…等字符串,并对这些字符串分别进行判断。不难发现,很多短子字符串在长些的子字符串中比较过,这导致了大量的冗余判断,根本原因是:对字符串对称的判断是由外向里进行的。
换一种思路,从里向外来判断。也就是先判断子字符串(如dd)是不是对称的。如果它(dd)不是对称的,那么向该子字符串两端各延长一个字符得到的字符串肯定不是对称的。如果它(dd)对称,那么只需要判断它(dd)两端延长的一个字符是不是相等的,如果相等,则延长后的字符串是对称的。
2.改进的解决方案
根据从里向外比较的思路写出如下代码:
- //改进后的程序
- int getMaxSym2(char * str)
- {
- if(str == NULL)
- return 0;
- int maxlength = 0;
- char *ptag = str;
- while(*ptag !='\0')
- {
- //奇数子字符串
- char *left = ptag - 1;
- char *right = ptag + 1;
- int oddlenght = 1;
- while(left >= str && *right != '\0' && *left == *right)
- {
- left--;
- right++;
- oddlenght += 2;
- }
- if(oddlenght > maxlength)
- {
- maxlength = oddlenght;
- }
- //偶数子字符串
- left = ptag;
- right = ptag + 1;
- int evenlength = 0;
- while(left >= str && *right != '\0' && *left == *right)
- {
- left--;
- right++;
- evenlength += 2;
- }
- if(evenlength > maxlength)
- {
- maxlength = evenlength;
- }
- ptag++;
- }
- return maxlength;
- }
//改进后的程序
int getMaxSym2(char * str)
{
if(str == NULL)
return 0;
int maxlength = 0;
char *ptag = str;
while(*ptag !='\0')
{
//奇数子字符串
char *left = ptag - 1;
char *right = ptag + 1;
int oddlenght = 1;
while(left >= str && *right != '\0' && *left == *right)
{
left--;
right++;
oddlenght += 2;
}
if(oddlenght > maxlength)
{
maxlength = oddlenght;
}
//偶数子字符串
left = ptag;
right = ptag + 1;
int evenlength = 0;
while(left >= str && *right != '\0' && *left == *right)
{
left--;
right++;
evenlength += 2;
}
if(evenlength > maxlength)
{
maxlength = evenlength;
}
ptag++;
}
return maxlength;
}
由于子字符串的长度可能是奇数也可能是偶数。长度是奇数的字符串是从只有一个字符的中心向两端延长出来,而长度为偶数的字符串是从一个有两个字符的中心向两端延长出来。因此程序中要把这两种情况都考虑进去。
由于总共有O(n)个字符,每个字符可能延长O(n)次,每次延长时只需要O(1)就能判断出是不是对称的,因此整个函数的时间效率是O(n^2)。
上述方法称为朴素算法,关于字符串的题目常用的算法有KMP、后缀数组、AC自动机,这道题目利用扩展KMP可以解答,其时间复杂度也很快O(N*logN)。但是,这里介绍一个专门针对回文子串的算法,其时间复杂度为O(n),这就是manacher算法。
3.manacher算法
算法的基本思路是这样的:把原串每个字符中间用一个串中没出现过的字符分隔#开来(统一奇偶),同时为了防止越界,在字符串的首部也加入一个特殊符,但是与分隔符不同。同时字符串的末尾也加入‘\0’。算法的核心:用辅助数组p记录以每个字符为核心的最长回文字符串半径。也就是p[i]记录了以str[i]为中心的最长回文字符串半径。P[i]最小为1,此时回文字符串就是字符串本身。示例:原字符串’abba’,处理后的新串’#a#b#b#a#\0’,得到对应的辅助数组p=[0,1,1,2,1,2,5,2,2,1]。
程序如下,对应的变量解释在后面
- //预处理,将str:abba转换为: $#a#b#b#a#\0(从1开始)
- char * pre(char *str)
- {
- int length = strlen(str);
- char *prestr = new char[2*length + 4];
- prestr[1] = '$';
- for(int i=0;i<length;i++)
- {
- prestr[2*(i+1)] = '#';
- prestr[2*(i+1)+1] = str[i];
- }
- prestr[2*length+2]='#';
- prestr[2*length+3]='\0';
- return prestr;
- }
//预处理,将str:abba转换为: $#a#b#b#a#\0(从1开始)
char * pre(char *str)
{
int length = strlen(str);
char *prestr = new char[2*length + 4];
prestr[1] = '$';
for(int i=0;i<length;i++)
{
prestr[2*(i+1)] = '#';
prestr[2*(i+1)+1] = str[i];
}
prestr[2*length+2]='#';
prestr[2*length+3]='\0';
return prestr;
}
以下是manacher算法的具体实现,包括:辅助数组的构建、最大字符串长度的获取。
- //manacher算法
- int getMaxSym3(char *str)
- {
- char *prestr = pre(str);
- int mx =0, pi=1;//边界和对称中心
- int len = strlen(prestr);
- //辅助数组
- int *p = new int[len];
- p[0] = 0;
- for(int i=1;i<len;i++)
- {
- if(mx>i)
- {
- p[i]=min(mx-i,p[2*pi-i]);//核心
- }
- else
- {
- p[i]=1;
- }
- while(prestr[i-p[i]]==prestr[i+p[i]]&&i-p[i]>0&&i+p[i]<len)
- {
- p[i]++;
- }
- if(i+p[i] > mx)
- {
- mx = p[i] + i;
- pi = i;
- }
- }
- //最大回文字符串长度
- int maxlen = 0;
- for(int i=0;i<len;i++)
- {
- if(p[i]>maxlen)
- {
- maxlen = p[i];
- }
- }
- delete []prestr;
- delete []p;
- return maxlen - 1;
- }
}
上面几个变量说明:pi记录具有遍历过程中最长半径的回文字符串中心字符串。mx记录了具有最长回文字符串的右边界。
pi是最长回文字符串(淡蓝色)的中心,如果以j为中心的最大回文串如上如所示,那么i处的情况与j处相同(关于pi的两侧是对称的)。这样便减少了运算量,i的对称位置是2*pi-i。
但是有另外一种情况,就是j的一部分超出蓝色部分,这时p[i]=p[j]就不一定对了,如下图
这就为什么有取最小值这个操作:
if(mx>i){ p[i]=min(mx-i,p[2*pi-i]);//核心}
剩下的代码就很容易看懂了。
最后遍历一边p数组,找出最大的p[i]-1就是所求的最长回文字符串长度,说明如下:
(1)因为p[i]记录插入分隔符之后的回文字符串半径,所以以i为中心的回文字符串长度为2*p[i]-1。例如:bb=>#b#b#,中间#的半径为3,回文字符串长度为2*3-1;
(2)注意上面两个串的关系。 #b#b#减去一个#号的长度就是原来的2倍。即((2*p[i]-1)-1)/2 = p[i]-1,得证。
和如下文章:
http://blog.163.com/kevinlee_2010/blog/static/169820820201111210740329/
问题描述:如题,给定一个字符串str和其长度n,求该字符串的一个最长公共回文子串的长度(公共子串个公共子序列是两个不同的概念)。并打印出该回文子串。
解答:1,首先给出一个比较直观的解法。根据回文的性质,我们可以把str进行逆转得到str1,然后求str和str1的最长公共子串,那么该子串的长度就是str的最长回文子串的长度,该公共子串就是最长的那个回文子串。也即我们把这个题目转化为求两个字符串str和str1的最长公共子串的问题。我们假设C[i,j]为以str[i]和str1[j]结尾的最长公共子串的长度,那么状态转移方程为:
C[i,j]=0 if(str[i]!=str1[j])
C[i,j]=c[i-1,j-1]+1 if(str[i]==str1[j])
初始条件为C[0,0]=0;最初的时间代价为O(n^2),空间代价为O(n^2),空间代价还可以优化为O(n),只需要j的循环式从高往低即可。代码如下:
//求str1和str2的最长公共子串,下标从1算起
void LCS_continue(char *str1,int n1,char *str2,int n2){
int **C=new int*[n1+1];
for(int i=0;i<=n1;i++){
C[i]=new int[n2+1]();
}
int max=0;
int max_index_i=0;
int max_index_j=0;
for(int i=1;i<=n1;i++){
for(int j=1;j<=n2;j++){
if(str1[i]==str2[j])
C[i][j]=C[i-1][j-1]+1;
else
C[i][j]=0;
if(C[i][j]>max){
max=C[i][j];
max_index_i=i;
max_index_j=j;
}
}
}
cout<<"最长公共子串长度为:"<<max<<ends;
if(max>0){
cout<<"子串为:"<<ends;
while(str1[max_index_i]==str2[max_index_j--]){
cout<<str1[max_index_i--]<<ends;
}
}
//free mem
for(int i=0;i<=n1;i++){
delete [] C[i];
}
delete [] C;
}
空间优化:
void LCS_continue_OPM(char *str1,int n1,char *str2,int n2){
int *C=new int[n2+1]();
int max=0;
int max_index_j=0;
for(int i=1;i<=n1;i++){
for(int j=n2;j>0;j--){
if(str1[i]==str2[j])
C[j]=C[j-1]+1;
else
C[j]=0;
if(C[j]>max){
max=C[j];
max_index_j=j;
}
}
}
cout<<"最长公共子串长度为:"<<max<<ends;
if(max>0){
cout<<"子串为:"<<ends;
while(max>0){
cout<<str2[max_index_j--]<<ends;
max--;
}
}
//free mem
delete [] C;
}
2,下面给出一个直观的动规解,
以下内容转自:http://blog.163.com/zhaohai_1988/blog/static/2095100852012716105847112/
方法二 动态规划 时间复杂度O(N2), 空间复杂度O(N2)
动态规划就是暴力法的进化版本,我们没有必要对每一个子串都重新计算,看看它是不是回文。我们可以记录一些我们需要的东西,就可以在O(1)的时间判断出该子串是不是一个回文。这样就比暴力法节省了O(N)的时间复杂度哦,嘿嘿,其实优化很简单吧。
P(i,j)为1时代表字符串Si到Sj是一个回文,为0时代表字符串Si到Sj不是一个回文。
P(i,j)= P(i+1,j-1)(如果S[i] = S[j])。这是动态规划的状态转移方程。
P(i,i)= 1,P(i,i+1)= if(S[i]= S[i+1])
string longestPalindromeDP(string s) {
int n = s.length();
int longestBegin = 0;
int maxLen = 1;
bool table[1000][1000] = {false};
for (int i = 0; i < n; i++) {
table[i][i] = true; //前期的初始化
}
for (int i = 0; i < n-1; i++) {
if (s[i] == s[i+1]) {
table[i][i+1] = true; //前期的初始化
longestBegin = i;
maxLen = 2;
}
}
for (int len = 3; len <= n; len++) {
for (int i = 0; i < n-len+1; i++) {
int j = i+len-1;
if (s[i] == s[j] && table[i+1][j-1]) {
table[i][j] = true;
longestBegin = i;
maxLen = len;
}
}
}
return s.substr(longestBegin, maxLen);
}
方法三 中心扩展法
这个算法思想其实很简单啊,时间复杂度为O(N2),空间复杂度仅为O(1)。就是对给定的字符串S,分别以该字符串S中的每一个字符C为中心,向两边扩展,记录下以字符C为中心的回文子串的长度。但是有一点需要注意的是,回文的情况可能是 a b a,也可能是 a b b a。
string expandAroundCenter(string s, int c1, int c2) {
int l = c1, r = c2;
int n = s.length();
while (l >= 0 && r <= n-1 && s[l] == s[r]) {
l--;
r++;
}
return s.substr(l+1, r-l-1);
}
string longestPalindromeSimple(string s) {
int n = s.length();
if (n == 0) return "";
string longest = s.substr(0, 1); // a single char itself is a palindrome
for (int i = 0; i < n-1; i++) {
string p1 = expandAroundCenter(s, i, i);
if (p1.length() > longest.length())
longest = p1;
string p2 = expandAroundCenter(s, i, i+1);
if (p2.length() > longest.length())
longest = p2;
}
return longest;
}
方法四 传说中的Manacher算法。时间复杂度O(N)
这个算法有一个很巧妙的地方,它把奇数的回文串和偶数的回文串统一起来考虑了。这一点一直是在做回文串问题中时比较烦的地方。这个算法还有一个很好的地方就是充分利用了字符匹配的特殊性,避免了大量不必要的重复匹配。
算法大致过程是这样。先在每两个相邻字符中间插入一个分隔符,当然这个分隔符要在原串中没有出现过。一般可以用‘#’分隔。这样就非常巧妙的将奇数长度回文串与偶数长度回文串统一起来考虑了(见下面的一个例子,回文串长度全为奇数了),然后用一个辅助数组P记录以每个字符为中心的最长回文串的信息。P[id]记录的是以字符str[id]为中心的最长回文串,当以str[id]为第一个字符,这个最长回文串向右延伸了P[id]个字符。
原串: w aa bwsw f d
新串: # w # a # a # b # w # s # w # f # d #
辅助数组P: 1 2 1 2 3 2 1 2 1 2 1 4 1 2 1 2 1 2 1
这里有一个很好的性质,P[id]-1就是该回文子串在原串中的长度(包括‘#’)。如果这里不是特别清楚,可以自己拿出纸来画一画,自己体会体会。当然这里可能每个人写法不尽相同,不过我想大致思路应该是一样的吧。
现在的关键问题就在于怎么在O(n)时间复杂度内求出P数组了。只要把这个P数组求出来,最长回文子串就可以直接扫一遍得出来了。
那么怎么计算P[i]呢?该算法增加两个辅助变量(其实一个就够了,两个更清晰)id和mx,其中id表示最大回文子串中心的位置,mx则为id+P[id],也就是最大回文子串的边界。
然后可以得到一个非常神奇的结论,这个算法的关键点就在这里了:如果mx > i,那么
P[i] >= MIN(P[2 * id - i], mx - i)。就是这个串卡了我非常久。实际上如果把它写得复杂一点,理解起来会简单很多:
//记j = 2 * id - i,也就是说 j 是 i 关于 id 的对称点。
if (mx - i > P[j])
P[i] = P[j];
else /* P[j] >= mx - i */
P[i] = mx - i; // P[i] >= mx - i,取最小值,之后再匹配更新。
当 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之后的部分是否对称,就只能老老实实去匹配了。
由于这个算法是线性从前往后扫的。那么当我们准备求P[i]的时候,i以前的P[j]我们是已经得到了的。我们用mx记在i之前的回文串中,延伸至最右端的位置。同时用id这个变量记下取得这个最优mx时的id值。(注:为了防止字符比较的时候越界,我在这个加了‘#’的字符串之前还加了另一个特殊字符‘$’,故我的新串下标是从1开始的)
#include<vector>
#include<iostream>
using namespace std;
const int N=300010;
int n, p[N];
char s[N], str[N];
#define _min(x, y) ((x)<(y)?(x):(y))
void kp()
{
int i;
int mx = 0;
int id;
for(i=n; str[i]!=0; i++)
str[i] = 0; //没有这一句有问题。。就过不了ural1297,比如数据:ababa aba
for(i=1; i<n; i++)
{
if( mx > i )
p[i] = _min( p[2*id-i], p[id]+id-i );
else
p[i] = 1;
for(; str[i+p[i]] == str[i-p[i]]; p[i]++)
;
if( p[i] + i > mx )
{
mx = p[i] + i;
id = i;
}
}
}
void init()
{
int i, j, k;
str[0] = '$';
str[1] = '#';
for(i=0; i<n; i++)
{
str[i*2+2] = s[i];
str[i*2+3] = '#';
}
n = n*2+2;
s[n] = 0;
}
int main()
{
int i, ans;
while(scanf("%s", s)!=EOF)
{
n = strlen(s);
init();
kp();
ans = 0;
for(i=0; i<n; i++)
if(p[i]>ans)
ans = p[i];
printf("%d\n", ans-1);
}
return 0;
}
if( mx > i)
p[i]=MIN( p[2*id-i], mx-i);
就是当前面比较的最远长度mx>i的时候,P[i]有一个最小值。这个算法的核心思想就在这里,为什么P数组满足这样一个性质呢?
(下面的部分为图片形式)
LEETCODE上也有这个题的详细说明,不过是英文版本的。
http://www.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html
和如下文章:
http://blog.youkuaiyun.com/alongela/article/details/8208303
回文串:如果某个字符串从左往右看与从右往左看是一样的,则称该字符串为回文串。
最长回文子串:求解一个字符串,它是某个给定字符串的子串,并且是回文串。
本题要求对于给定的字符串,求出它的最长回文子串的长度。可以采用Manacher算法在O(n)时间内求解。
[cpp] view plaincopy
1. #include <iostream>
2. #include <cstdio>
3. #include <cstring>
4. #include <algorithm>
5. using namespace std;
6.
7. const int N = 1000005;
8. char str[N]; //原字符串
9. char s[N << 1]; //用'#'作为间隔填充之后的字符串
10. int p[N << 1]; //p[i]表示以第i个字符为中心的回文子串的长度加1
11. int len;
12. int ans; //最长回文子串的长度
13.
14. void Manacher()
15. {
16. int i;
17. int mx; //mx表示当前回文子串扩展的最右端
18. int id; //id表示mx是由哪个回文子串扩展出来的
19. len = (strlen(str) + 1) << 1;
20. for (i = 0; i < len; ++i) //构造填充字符串
21. {
22. s[i] = '#';
23. p[i] = 0;
24. }
25. for (i = 0; str[i] != 0; ++i) s[(i + 1) << 1] = str[i];
26. s[(i + 1) << 1] = 0;
27. mx = 0;
28. ans = 0;
29. for (i = 1; s[i] != 0; ++i) //求解p数组
30. {
31. if (mx > i) p[i] = min(mx - i, p[(id << 1) - i]);
32. else p[i] = 1;
33. while (s[i - p[i]] == s[i + p[i]]) ++p[i];
34. if (i + p[i] > mx)
35. {
36. mx = i + p[i];
37. id = i;
38. }
39. if (p[i] - 1 > ans) ans = p[i] - 1; //更新最长回文子串的长度
40. }
41. }
42.
43. int main()
44. {
45. int Case = 0;
46. while (scanf("%s", str) != EOF)
47. {
48. if (strcmp(str, "END") == 0) break;
49. Manacher();
50. printf("Case %d: %d\n", ++Case, ans);
51. }
52. return 0;
53. }