manacher马拉车算法(学习笔记)

推荐一个博客学习

例题:给出一个只由小写英文字符a,b...y,z组成的字符串S,求S中最长回文串的长度.字符串长度为n;

由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,具体做法是:在字符串首尾,及各字符间各插入一个字符(前提这个字符未出现在串里),这样以后回文串的长度必定是奇数。例如:

bccb ----> ~|b|c|c|b|

定义一个数组int f[],其中f[i]表示以 i 为中心的最长回文的半径;例如字符串bcacb(这个地方不能理解的,自己在纸上举几个例子就明白了):

f[1]=1, f[2]=1, f[3]=3, f[4]=1, f[5]=1;

设置两个变量maxr 和 mid 。maxr 代表以 mid 为中心的最长回文的右边界,也就是maxr = mid + f[mid]。

注意:maxr不是我们读入的字符串的右边界,而是我们当前已知的回文串的右边界,我们就是要不断使它往右扩展,即回文串长度不断增大

假设我们现在求f[i],也就是求以 i 为中心的最长回文半径,如果i < maxr,那么:


if(i<maxr){

  f[i]=min(f[(mid<<1)-i],maxr-i);

}

2 * mid - i是 i 关于 mid 的对称点(可根据中点公式证明),设i,j关于mid对称,则(i+j)/2=mid,j=2*mid-i;

f[j]表示以 j 为中心的最长回文半径,因此我们可以利用f[j]来加快查找。


#include<bits/stdc++.h>
using namespace std;
int cnt,maxr,mid,f[25000005];

//cnt表示读入字符个数,maxr,mid,f数组意义如上文所述,
int maxans;                           
//记录答案,即最长回文串的长度
char ch,s[25000005];          
//ch循环读入字符所用,s数组储存读入的字符串
int main(){
     s[++cnt]='~';            
//使字符串第一个字符为'~',具体作用请见下面代码
     s[++cnt]='|';      
     while(~scanf("%c",&ch)){
          if(ch>='a'&&ch<='z'){
             s[++cnt]=ch;
             s[++cnt]='|';         
  //读入字符串的同时,读入分隔符
       }
       else break;
     }
     for(int i=1;i<=cnt;i++){   
  //枚举读入的每个字符
       if(i<maxr) 
             f[i]=min(f[(mid<<1)-i],maxr-i); 
 //maxr表示以 mid 为中心的最长回文串的右边界;
//(mid<<1)-i表示i关于mid的对称点;
//maxr-i表示半径可达到的最大值(i+f[i]<=maxr);
       else f[i]=1;                                              
 //否则该字符本身构成一个独立的回文串,半径为1
       while(s[i-f[i]]==s[i+f[i]]) 
       	  f[i]++;                 
//以i为中心,i-f[i],i+f[i]分别是回文串左右两边端点
//如果相等,说明以i为中心的回文串的半径还可以继续增大
//此处体现了我们之前使字符串0号下标为字符'~'的作用
//防止数组越界,使循环暂停;
       if(f[i]+i>maxr) maxr=f[i]+i,mid=i;           
//如果可以更新右边界maxr,则更新右边界;
//同时也要更新mid(mid随maxr变化);
       if(f[i]>maxans) maxans=f[i];                 
//更新最大答案
     }
     printf("%d",maxans-1);                                
//输出ans-1,为什么要减1,自己手动举几个例子就明白了
     return 0;
}

练习:对于一个长度为偶数的序列\(a_1,a_2,a_3...a_n\),定义这个序列为好的序列,当且仅当\(a_1+a_n=a_2+a_{n-1}=a_3+a_{n-2}...\)。再定义一个对序列的翻滚操作,使所有元素向前移一个位置,第一个元素移到最后的位置。现在给你一个长度为偶数的序列\(b_1,b_2,b_3...b_n\),请问至少需要翻滚多少次才能使这个序列成为好的序列?

#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
#define db double
using namespace std;
int l[2000005],a[4000005];
int n,i,mid,r,t;
ll tot;
inline int read(){
    char ch;
    while((ch=getchar())<'0'||ch>'9');
    	int res=ch^48;
    while((ch=getchar())>='0'&&ch<='9')
    	res=(res<<1)+(res<<3)+(ch^48);
    return res;
}
int main(){
    n=read();
    for(i=1;i<=n;++i){
        a[i+n]=a[i]=read();
        tot+=a[i];
    }
    //数组倍长,计算总和tot
    t=tot/(n/2);
    //计算每对匹配元素的和
    i=0;
    a[0]=-100000000;
    //a[0]赋极大值,作用于下面循环停止
    while(i<=n){//马拉车模板
    	if(i<r)
            l[i]=min(l[(mid<<1)-i],r-i);
        while(a[i+l[i]]+a[i-l[i]+1]==t)
            l[i]++;
        if(l[i])
            l[i]--;
        if(l[i]*2>=n){
            printf("%d\n",i-(n/2));
            return 0;
        }
        if(i+l[i]>r){
            r=i+l[i];
            mid=i;
        }
    	i++;
    }
    printf("IMPOSSIBLE\n");
    return 0;
}

posted on 2018-10-27 14:29  PPXppx  阅读(172)  评论(0编辑  收藏  举报