Manacher
解决问题:
字符串str中,最长回文子串的长度如何求解
要求:时间复杂度为O(N)
一开始思路(经典解法):每一个字符从中心开始扩散对比左右并记录长度
有问题:长度为偶数的回文没办法得到(轴是虚的),只有奇数的回文能得到
解决方法:在每两个字符之间加一个字符#(不一定要求原字符串中没出现过的字符)(实的跟实的比,虚的跟虚的比,不会出现实的和虚的比)
这样可以得到答案,最终答案需要/2
时间复杂度:O(N^2):最差情况:全部字符都相等
Manacher(时间复杂度:O(N)思路与经典解法类似,但是在过程中有优化(与kmp类似)
几个概念:
- 回文直径:从中心往左右两边扩出来的整个区域的大小
- 回文半径:回文直径/2
- 之前所扩的位置中所到达的最右回文右边界R:该数初始值为-1,不管是哪个位置扩的,只要扩的范围的右边界比当前数大,就替换
- 变量C:跟R同步更新,初始值为-1,取得最右回文右边界时中心的位置
第一种情况:
当前点的位置不在最右回文右边界R的范围里(包括当前点刚好与R重合),暴力扩(跟经典做法一样)(没有优化)
第二种情况:
当前点的位置在最右回文右边界R的范围里,此时C一定在i的左侧,i可能与R重合,根据R与C可以做出L(回文半径),根据C与i可以做出i’
根据i位置的回文状况对第二大类情况进行分类:
- i‘位置的回文区域整个都在L到R区域的内部,此时的i位置的回文区域大小跟i’一样,如图,此时乙区域是甲区域的逆序(L到R为回文),甲是回文串,至此i位置的回文长度至少是i’的回文长度,再根据图中的三个条件判断得到i位置的回文长度与i’位置的相等
- i’的回文区域一部分在L到R外面,此时i的回文长度为i到R,证明过程如图
- i‘的回文区域的左边界与L重合,i的回文长度至少等于i’的回文长度,可能会变得更大,要看左右部分(小加速)
时间复杂度的证明根据i和R两个变量来估计分析,四个循环分别分析,i和R的最大值都是N,每个分支结束i都会++,R只有在暴力扩的时候才有可能增加(即i和R都是只增不退的)四个分支都在for循环里,每次只会执行一个,第一个分支和第四个分支可能会扩失败一次,第二和第三个分支不会扩,即每个位置最多只会扩失败一次,所以扩失败的代价是O(N),而R变大的总幅度就是扩成功的次数(R如果等于字符串的总长度N循环就可以结束了?),即扩成功的代价是O(N)(扩成功R至少+1),另外两个分支时间复杂度是O(1),因此整个过程时间复杂度是O(N)
public int maxLcpsLength(String s){
if(s==null||s.length()==0){
return 0;
}
char[] str=manacherString(s);//加特殊字符
int[] pArr=new int[srt.length];//回文半径数组
int C=-1;//中心位置
int R=-1;//注意 这里为了coding,R代表的是回文右边界的再往右一个位置,最右的有效区是R-1位置
int max=Integer.MIN_VALUE;//扩出来的最大值
for(int i=0;i!=str.length;i++){//注意这里的终止条件
pArr[i]=R>i?Math.min(pArr[2*C-i],R-i):1;//i至少的回文半径(至少不用验证的区域),先给pArr[i] 2*C-i就是i'
while(i+pArr[i]<str.length&&i-pArr[i]>-1){//四种情况都看看能不能往外扩,第二第三分支不用往外扩,就算扩一次也会失败,增加的是一点点常数时间(不会改变时间复杂度),带来的是if-else的减少(代码简洁性),追求极致性能也可以增加
if(str[i+pArr[i]]==str[i-pArr[i]]){
pArr[i]++;
}else{
break;
}
}
if(i+pArr[i]>R){//更新R和i
R=i+pArr[i];
C=i;
}
max=Math.max(max,pArr[i]);//g
}
return max-1;//半径-1即真正的回文直径(有特殊字符)
}
public char[] manacherString(String str){
char[] charArr=str.toCharArray();
char[] res=new char[str.length*2+1];
int index=0;
for(int i=0;i!=res.length;i++){
res[i]=(i&1)==0?'#':charArr[index++];
}
return res;
}