KMP&扩展KMP&Manacher算法基础与习题(第三更)

KMP&扩展KMP&Manacher算法基础与习题(第一更)

KMP&扩展KMP&Manacher算法基础与习题(第二更)

目录

Manacher算法讲解

例题

A:HDU-3613 Best Reward

B:POJ-3974 Palindrome

C:HDU-4513 吉哥系列故事——完美队形II

D:HDU-3294 Girls' research

E:HDU-4763 Theme Section


Manacher算法讲解(转自Manacher算法讲解

算法思路

首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#(注意,下面的代码是用C语言写就,由于C语言规范还要求字符串末尾有一个'\0'所以正好OK,但其他语言可能会导致越界)。

下面以字符串12212321为例,经过上一步,变成了 S[] = "$#1#2#2#1#2#3#2#1#";

然后用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:

S  #  1  #  2  #  2  #  1  #  2  #  3  #  2  #  1  #
P  1  2  1  2  5  2  1  4  1  2  1  6  1  2  1  2  1
(p.s. 可以看出,P[i]-1正好是原字符串中回文串的总长度)


那么怎么计算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 的对称点(j = id - (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之后的部分是否对称,就只能老老实实去匹配了。
点击在新窗口中浏览此图片

对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。
于是代码如下:

//输入,并处理得到字符串s
int p[1000], mx = 0, id = 0;
memset(p, 0, sizeof(p));
for (i = 1; s[i] != '\0'; i++) {
    p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
    while (s[i + p[i]] == s[i - p[i]]) p[i]++;
    if (i + p[i] > mx) {
        mx = i + p[i];
        id = i;
    }
}
//找出p[i]中最大的

模板

int init(){//对原字符进行预处理
    newStr[0]='$';
    newStr[1]='#';
    int j=2;
    int len=strlen(str);
    for (int i=0;i<len;i++){
        newStr[j++]=str[i];
        newStr[j++]='#';
    }
    newStr[j] ='\0'; //字符串结束标记
    return j;//返回newStr的长度
}

int manacher(){
    int len=init();//取得新字符串长度并完成字符串的预处理
    int res=-1;//最长回文长度
    int id;
    int mx=0;
    for(int i=1;i<len;i++){
        int j=2*id-i;//与i相对称的位置
        if(i<mx)
            p[i]=min(p[j], mx-i);
        else
            p[i]=1;
         //由于左有'$',右有'\0',不需边界判断
        while(newStr[i-p[i]] == newStr[i+p[i]])//p[i]的扩大
            p[i]++;
        if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
            id=i;
            mx=i+p[i];
        }
        res=max(res,p[i]-1);
    }
    return res;
}

例题

A:HDU-3613 Best Reward:题目的要求就是给你一个字符串让你把它分成两个字符串 ,一行给你26个字母的价值,如果分出来的子串是回文序列,那么它的价值就是序列所有字母价值的和,如果不是回文序列则价值为0,找最大的价值,扩展kmp:将母串s1分为T1,T2两个子串(T1为前半串,T2为后半串) 首先找到s1的倒串s2;用s1去匹配s2,判断T1是不是回文序列,通过s2匹配s1判断T2是不是回文序列。AC代码:

#include<stdio.h>
#include<string>
#include<string.h>
#include<iostream>
#include<algorithm>
using namespace std;
int v[27];
char s1[500005];
char s2[500005];
int nex[500005];
int ex1[500005];
int ex2[500005];
int sum[500005];
void Getnext(char *str)
{
    int i=0,j,po,len=strlen(str);
    nex[0]=len;
    while(str[i]==str[i+1]&&i+1<len)
        i++;
    nex[1]=i;
    po=1;
    for(i=2;i<len;i++)
    {
        if(nex[i-po]+i<nex[po]+po)
            nex[i]=nex[i-po];
        else
        {
            j=nex[po]+po-i;
            if(j<0)j=0;
            while(i+j<len&&str[i+j]==str[j])
                j++;
            nex[i]=j;
            po=i;
        }
    }
}
void EXKMP(char *s1,char *s2,int ex[])
{
    int i=0,j,po,len=strlen(s1),l2=strlen(s2);
    Getnext(s2);
    while(s1[i]==s2[i]&&i<len&&i<l2)
        i++;
    ex[0]=i;
    po=0;
    for(i=1;i<len;i++)
    {
        if(nex[i-po]+i<ex[po]+po)
            ex[i]=nex[i-po];
        else
        {
            j=ex[po]+po-i;
            if(j<0)j=0;
            while(i+j<len&&j<l2&&s1[i+j]==s2[j])
                j++;
            ex[i]=j;
            po=i;
        }
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        for(int i=0;i<26;i++)
            scanf("%d",&v[i]);
        scanf("%s",s1);
        int len=strlen(s1);
        sum[0]=0;
        for(int i=0;i<len;i++)
        {
            s2[i]=s1[len-i-1];
            sum[i+1]=sum[i]+v[s1[i]-'a'];
        }
        EXKMP(s2,s1,ex1);
        EXKMP(s1,s2,ex2);
        int ans=-500000;
        for(int i=1;i<len;i++)//计算价值
        {
            int tmp=0;
            if(i+ex1[i]==len)tmp+=sum[len-i];/*从i处分开,如果i(T2)+ex[i](T1)==len,说明T1是回文,T1的长度是(len-i),T1是前半部分所以价值是      sum[len-i];*/
            int pos=len-i;
            if(pos+ex2[pos]==len)tmp+=sum[len]-sum[pos];/*pos是T1的长度,如果pos(T1)+ex2[pos](T2)==len,说明T2是回文,T2是后半部分,所以价值是sum[len]-sum[pos]*/
            if(tmp>ans)
                ans=tmp;
        }
        printf("%d\n",ans);
    }
}

B:POJ-3974 Palindrome:求最大回文的长度,Manacher的模板题,AC代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
#define INF 0x3f3f3f3f
#define N 1000001
using namespace std;
char str[N];//原字符串
char newStr[N*2];//预处理后的字符串
int p[N*2];//辅助数组
int init(){//对原字符进行预处理
    newStr[0]='$';
    newStr[1]='#';
    int j=2;
    int len=strlen(str);
    for (int i=0;i<len;i++){
        newStr[j++]=str[i];
        newStr[j++]='#';
    }
    newStr[j] ='\0'; //字符串结束标记
    return j;//返回newStr的长度
}

int manacher(){
    int len=init();//取得新字符串长度并完成字符串的预处理
    int res=-1;//最长回文长度
    int id;
    int mx=0;
    for(int i=1;i<len;i++){
        int j=2*id-i;//与i相对称的位置
        if(i<mx)
            p[i]=min(p[j], mx-i);
        else
            p[i]=1;
         //由于左有'$',右有'\0',不需边界判断
        while(newStr[i-p[i]] == newStr[i+p[i]])//p[i]的扩大
            p[i]++;
        if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
            id=i;
            mx=i+p[i];
        }
        res=max(res,p[i]-1);
    }
    return res;
}

int main(){
    int Case=1;
    while(scanf("%s",str)!=EOF){
        if(str[0]=='E')
            break;
        printf("Case %d: %d\n",Case++,manacher());
    }
    return 0;
}

C:HDU-4513 吉哥系列故事——完美队形II:在manacher函数中加一个判断,跳过原来的加入的值,以及加一个判断控制最中间向两边满足非递增即可。具体参考代码。

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define N 1000001
using namespace std;
int str[N];//原字符串
int newStr[N*2];//预处理后的字符串
int p[N*2];//辅助数组
int n;
int init(){//对原字符进行预处理
    newStr[0]=-1;
    newStr[1]=-2;
    int j=2;
    int len=n;
    for (int i=0;i<len;i++){
        newStr[j++]=str[i];
        newStr[j++]=-2;
    }
    newStr[j] ='\0'; //字符串结束标记
    return j;//返回newStr的长度
}

int manacher(){
    int len=init();//取得新字符串长度并完成字符串的预处理
    int res=-1;//最长回文长度
    int id;
    int mx=0;
    for(int i=1;i<len;i++){
        int j=2*id-i;//与i相对称的位置
        if(i<mx)
            p[i]=min(p[j], mx-i);
        else
            p[i]=1;
         //由于左有'$',右有'\0',不需边界判断
        while(newStr[i-p[i]] == newStr[i+p[i]]){//p[i]的扩大
            if(newStr[i+p[i]]!=-2){
                if(newStr[i+p[i]]<=newStr[i+p[i]-2])
                    p[i]++;
                else
                    break;
            }
            p[i]++;
        }
        if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
            id=i;
            mx=i+p[i];
        }
        res=max(res,p[i]-1);
    }
    return res;
}

int main(){
    int t;
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=0;i<n;i++)
            scanf("%d",&str[i]);
        cout<<manacher()<<endl;
    }
    return 0;
}

D:HDU-3294 Girls' research:这道题的主要意思是给你第一个字母代表a,让你递推出这串字符真正的字符串是什么,然后叫你找出字符串里面回文串的开始与结束下标,然后打印出这串回文,主要是要知道新字符串在原本字符串中对应的位置,具体见代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
#define INF 0x3f3f3f3f
#define N 1000001
using namespace std;
char str[N];//原字符串
char newStr[N*2];//预处理后的字符串
int p[N*2];//辅助数组
int t;
int init(){//对原字符进行预处理
    newStr[0]='$';
    newStr[1]='#';
    int j=2;
    int len=strlen(str);
    for (int i=0;i<len;i++){
        newStr[j++]=str[i];
        newStr[j++]='#';
    }
    newStr[j] ='\0'; //字符串结束标记
    return j;//返回newStr的长度
}

int manacher(){
    int len=init();//取得新字符串长度并完成字符串的预处理
    int res=-1;//最长回文长度
    int id;
    int mx=0;
    for(int i=1;i<len;i++){
        int j=2*id-i;//与i相对称的位置
        if(i<mx)
            p[i]=min(p[j], mx-i);
        else
            p[i]=1;
         //由于左有'$',右有'\0',不需边界判断
        while(newStr[i-p[i]] == newStr[i+p[i]])//p[i]的扩大
            p[i]++;
        if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
            id=i;
            mx=i+p[i];
        }
        if(p[i]-1>res){
            res=p[i]-1;
            t=i;
        }
    }
    return res;
}

int main(){

    char n;
    while(scanf("%c",&n)==1){
        scanf("%s",str);
        int len=strlen(str);
        for(int i=0;i<len;i++){
                if(str[i]>=n){
                    str[i]=97+(str[i]-n);//把比b[i]大于等于的转化
                }
                else{
                    str[i]=123-(n-str[i]);//把比b[i]小的转化
                }
        }
        getchar();//注意这个很重要,我检查了半天才发现,否则第二次输入时会转化成一堆乱码
        int max1=manacher();
        if(max1>1){
            if(t%2==1){//分为两种情况对待,优势p[t]最大的时候刚好指着#号
                printf("%d %d\n",(t-1)/2-(max1)/2,(t-1)/2+(max1)/2-1);
                for(int i=(t-1)/2-(max1)/2;i<=(t-1)/2+(max1)/2-1;i++) printf("%c",str[i]);
                printf("\n");
            }
            else{
                printf("%d %d\n",t/2-(max1)/2-1,t/2+(max1)/2-1);
                for(int i=t/2-(max1)/2-1;i<=t/2+(max1)/2-1;i++) printf("%c",str[i]);
                printf("\n");
            }
        }
        else printf("No solution!\n");
    }
    return 0;
}

E:HDU-4763 Theme Section:给出一串主串,让求主串中EAEBA形式的E串的最大长度,必须要开头E串结尾E串,AB串长度任意,可以是0;思路:既然要求三个相同的子串,而且有两个还必须在开头和结尾,那就求Next数组,Next数组存的是前后缀相同的长度,所以只需要找【2*i,L-Next[i]】之间相同的字串,即Next[j]==i;具体代码:

#include<stdio.h>
#include<string.h>
#include<string>
#include<algorithm>
#include<math.h>
#include<map>
#include<queue>
#include<vector>
#include<stack>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int N=1000005;

char s[N];
int Next[N],l;

void get_Next()
{
    int i=0,j=-1;
    Next[0]=-1;
    while(i<l)
    {
        if(j==-1||s[i]==s[j])
            Next[++i]=++j;
        else
            j=Next[j];
    }
}

int KMP()
{
    int i,j;
    for(i=Next[l]; i; i=Next[i])
        for(j=2*i; j<=l-i; j++)
            if(Next[j]==i)
                return i;
    return 0;
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s",s);
        l=strlen(s);
        get_Next();
        printf("%d\n",KMP());
    }
    return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值