• 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语言规范还要求字符串末尾有一个''所以正好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] != ''; 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] =''; //字符串结束标记
        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;
             //由于左有'$',右有'',不需边界判断
            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
    ",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] =''; //字符串结束标记
        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;
             //由于左有'$',右有'',不需边界判断
            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
    ",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] =''; //字符串结束标记
        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;
             //由于左有'$',右有'',不需边界判断
            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] =''; //字符串结束标记
        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;
             //由于左有'$',右有'',不需边界判断
            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
    ",(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("
    ");
                }
                else{
                    printf("%d %d
    ",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("
    ");
                }
            }
            else printf("No solution!
    ");
        }
        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
    ",KMP());
        }
        return 0;
    }
  • 相关阅读:
    8月7号的练习:HDU 1069&&POJ 1636&&HDU 1031&&HDU 1051&&HDU 1551
    8月8号的线段树:HDU 1754&&POJ 3264&&HDU1166
    8月6号的题目:HDU 1003&& POJ 1050&&HDU 1800&&HDU 2036&& POJ 1088(记忆化搜索)
    HDU 1052
    背包问题九讲:
    一个人的旅行 HDU 2066 &&HDU Today HDU 2112
    8月3号的LCS,LIS,LICS:Longest Ordered Subsequence&&Common Subsequence&&Greatest Common Increasing Subsequence
    那些操蛋的搜索题目:逃离迷宫&&哈密顿绕行世界问题
    C语言栈调用机制初探
    linux0.11改进之四 基于内核栈的进程切换
  • 原文地址:https://www.cnblogs.com/shmilky/p/14089044.html
Copyright © 2020-2023  润新知