传统方法求最长回文字符串 时间复杂度(O(n))
#include <iostream>
using namespace std;
#include <cstring>
#include <cstdio>
#include<algorithm>
char b[110001];
int HUIWEN(int n)
{
int j=n-1;
int i=0;
while(i<n)
{
if(b[i]==b[j])
{
i++;
j--;
}
else
return 0;
}
return n;
}
int main()
{
while (scanf ("%s",b) != EOF)
{
int res=0;
int n=strlen(b);
for(int i=1;i<=n;i++)
{
res=max(HUIWEN(i),res);
}
cout<<res<<endl;
memset(b,0,sizeof(b));
}
return 0;
}
中心扩展算法
由外部向最内部比较 ——>由中心外部比较 减少一次遍历
可以用这种方法提高效率:
回文串一定是对称,所以我们可以每次循环都选择一个中心,进行左右扩展,判断左右字符是否相等即可。由于既存在奇数的字符串又存在偶数的字符串,所以要么中心从一个字符开始,要么从两个字符的中间开始。所以一共有n+n-1个中心 (从最左到最右)
所以代码的思路是遍历每个中心,然后判断对称位置是否相等。
时间复杂度 (O(n))(两层循环每次循环都是遍历每个字符 )
空间复杂度O(1)
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int expandAroundCenter(string s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.size() && s[L] == s[R]) {
L--;
R++;
}
return R - L - 1;
}
string longestPalindrome(string s) {
if (s.empty() || s.size() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.size(); i++) {
int len1 = expandAroundCenter(s, i, i); //从一个字符扩展
int len2 = expandAroundCenter(s, i, i + 1); //从两个字符之间扩展
int len = max(len1, len2);
//根据 i 和 len 求得字符串的相应下标
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substr(start, end + 1);
}
int main()
{
string n;
cin>>n;
cout<< longestPalindrome(n);
return 0;
}
传统方法反思
要分奇数和偶数进行考虑
没有思考利用前面查找工作的结果
没有思考会问字符本身的特性——对称性
所以我们的重头戏来了
马拉车算法(Manacher)
在原字符串的每个相邻两个字符中间插入一个分隔符,同时在收尾也要加入一个分隔符,可以插入‘#’
经过这样转换 永远都是一个奇数 这样对称中心就是唯一的
string charu(string s)//首先构造出新的字符串
{
string Str ="*#";//以*#开头,#结尾 为了方便先在#前面打一个无关字符 如果不这样最后max(p)会出问题
for(int i=0;i<s.size();i++)
{
Str+=s[i];
Str+='#';
}
return Str;
}
之后就是马拉车的精髓部分
一开始我也是不太懂
经过上面插入特殊字符后,我们再求得其长度,这时候
我们定义p[i]表示i位置的最长回文子串的长度
例如字符串abbbba 插入字符后为*#a#b#b#b#b#a# 长度为14
用P的下标i减去P[i]再除以2,就是原字符串的开头下标。
比如abbbba中的第2个b序号为2 等于(6-3)/2=1 所以以这个b为中心的最大回文字符串的起点下标为1 则想将其输出 只需要输出字符串的第1位到第3(i-1)位 即 “bbb”
但是很明显这并不是最长的回文字符串,我们要用回文串的对称性将每个P[ i ](i位置的最长回文子串的长度 )求出来
我们用C表示回文串的中心,用R表示回文串的右边半径。所以R=C+P[i]
C和R所对应的回文串是当前循环中R最靠有的回文串
让我们考虑求P[i]的时候
用mirrori表示当前需要求的第i的字符关于C对应的下标
由对称性P[i]=P[mirrori] 例如当C为7 P[6]=P[8]=3
但是有以下三种情况不适合直接用对称性
1.超出了R
当C=8 P[6]与P[10]对称 为什么值不相等呢 因为C往右加上6等于14
最右边的回文字符串为bbb所以R为最后一个b右边一位为11 而14大于11 所以不行
但是我们P[i]至少可以达到R-i
会不会更大 我们只需要对比R以后的点和其关于i对称的点是否相等
就像暴力从中心向两边扩展一样
2.P[mirrori碰到了原字符串的左边界
比如当miorrori=2时候 P[2]与P[6]对称 但是P[2]!=P[6]
原因是P[mirrori]在扩展时候首先是“#”=“#”,之后遇到“*”和另一个字符到达边界终止循环
而P[i](右边的)对称点并没有到达边界,所以我们可以继续通过中心扩展法一步一步向两边扩展
就可以了
3.i等于了R
此时我们把P[0]赋值为0,再通过中心法扩展一步一步向两边扩展
就可以了
考虑C和R的更新
当如上面方法一步一步的求出每个P[i],当求出的P[i]右边界大于当前的R时,我们就需要更新C和R为当前的回文串了,因为我们要保证i在R里面,所以一旦有更右边的R就要更新R
此时发现经计算P[mirrori] 应该得3 所以此时C应该变为i=10 半径变为i+P [ i ]=13 然后继续下边的循环
#include <iostream>
using namespace std;
string charu(string s)//首先构造出新的字符串
{
string Str ="*#";//以*#开头,#结尾 为了方便先在#前面打一个无关字符 如果不这样最后max(p)会出问题
for(int i=0;i<s.size();i++)
{
Str+=s[i];
Str+='#';
}
return Str;
}
string malache(string s)
{
string t=charu(s);
int n=t.size();
int P[n];
int C=0,R=0;
for(int i=1;i<n;i++)
{
int mirrori=2*C-i;
if(R>i)
{
P[i]=min(R-i,P[mirrori]);
}
else
{
P[i]=0;
}
while(t[i+1+P[i]]==t[i-1-P[i]])
{
P[i]++;
}
if(i+P[i]>R){
C=i;
R=i+P[i];
}
}
int maxlen=0;
int centerindex=0;
for(int i=1;i<n-1;i++)
{
if(P[i]>maxlen)
{
maxlen=P[i];
centerindex=i;
}
}
int start=(centerindex-maxlen)/2;
return s.substr(start,start+maxlen);
}
int main()
{
string a;
cin>>a;
cout<<malache(a);
return 0;
}