小伙伴们大家好,今天给大家带来一道算法题:如何找一个字符串中的最大回文长度。何为回文?简单来讲就是正着读和倒着读结果相同,如aba。
暴力算法
给定一个字符串s=“abac”,经典的暴力算法思想是对每个字符进行回文串扩充。i=0,对a进行扩充,发现其左边没有元素,因此回文长度为0。i=1时,对b进行扩充,发现其左右元素均为a,再对两边a进行扩充,但扩充均失败,因此i=1时回文长度为3。同理i=2,i=3。
并且这种方法存在一个弊端,比如字符串等于122131221。虽然我们能找到最大回文为122131221,但是1221这个回文串却被我们忽视了,它无法找到偶数长度回文串。这是因为偶数长度回文其中心轴为虚轴。
那该如何进行改进呢?答案是为每个字符前后添加字符#,上述字符串变#1#2#2#1#3#1#2#2#1,这样再对每个字符进行扩充时无论回文长度为奇数还是偶数都可以找出了(扩充时包含#)。比如回文1221就包含在第二个#的扩充回文串里:1#2#2#1。
那我们添加的字符必须为#吗?必须为特殊字符吗?答案是否定的。其实是任意的,你可以在每个字符前后均添加1,2......为什么呢?因此我们添加了字符后,每次进行扩充比较时,均为原字符比原子符,扩充字符比扩充字符。扩充字符对原子符的扩充没有障碍,因此随意(但要保持相等,你不能一个补充1一个补充2这种)。
Manacher算法
算法分析
Manacher算法是对上述暴力扩充的一种优化。其将扩充情况分为两大类,第二大类又分为三个小类。不过在此之前,我们需要认识下述几个概念。
R:扩充回文长度的右边界,初始时为-1。比如12321(其实还有添加的额外字符这里方便观看省略),i=0时回文串范围为0~0,R=0>-1,更新为0。i=1时,回文串范围为1~1>0,更新为1。i=2时,扩充回文串为12321,范围为0~4>1,更新R为4。i=3时,回文范围为3~3<4,不更新。i=4时,R=4~4=4,不更新。
C:当R改变时,C也进行改变,初始时为-1。C为此R下的回文串中心。如i=2时,R变为4发生更新,则C更新为2。
回文半径:如i=2时,回文串为12321,则回文半径为3。(123)
第一类:当前位置落在R之外
很简单,直接暴力扩,无优化。如123,初始时R=-1,i=0时发现其落在R之外,直接暴力扩,更改R=0。i=1时落在R之外,继续暴力扩。
第二类:当前位置落在R里
存在下图关系:
在此基础上继续分类:
1>i‘位置回文序列在L-R内部
如下图:
这种情况,我们无需对i位置进行求解,只需一步即可,完成对暴力算法的优化。
2>i’位置回文序列在L-R之外
如下图:
这种情况,我们也可以直接得到i位置回文情况,完成对暴力算法的优化。
3>i‘回文落在L-R边界
代码展示
#include<iostream>
using namespace std;
string manacherstring(string s){
string str="";
for(int i=0;i<s.length();i++){
if(i==0){
str.push_back('#');
}
str.push_back(s[i]);
str.push_back('#');
}
return str;
}
int maxlength(string s){
int R=-1,C=-1;//R-1为回文右边界
int parr[s.length()];
for(int i=0;i<s.length();i++){
//至少不用验证的区域先赋值给parr数组
parr[i]=R>i?min(parr[2*C-i],R-i):1;
//看看能否继续扩充(公共部分虽然有些情况无需扩充)
while(i+parr[i]<s.length()&&i-parr[i]>=0){
if(s[i+parr[i]]==s[i-parr[i]]){
parr[i]++;
}
else{
break;
}
}
if(i+parr[i]>R){
R=i+parr[i];
C=i;
}
}
int max=-1;
for(int i=0;i<s.length();i++){
if(max<parr[i]){
max=parr[i];
}
}
return max-1;
}
int main(){
string s="123513421";
s=manacherstring(s);
int length=maxlength(s);
cout<<length;
}
for循环第一句解释:
返回max-1:大家可以自己举个例子(因为原串中含有#,并且parr为半径)。
分享到此结束,感谢大家支持!!