KMP总结

突然发现模板挺重要的,用别人写的模板去写题感觉有点不舒服,还是自己总结一下出个板子好了,适合自己的才是最好的。就先从kmp这个专题开始总结吧。

自己的模板

判断模式串在总串的哪个位置开始出现

#include <iostream>
#include<algorithm>
#include<string.h>
#include<stdio.h>
using namespace std;
int ne[10005];
int s[1000005],p[10005];
int m,n;
void getnext()
{
    int i=-1,j=0;     //i是头,j是尾,然后往后面跳
    ne[0]=-1;      //判断是否跳到头
    while(j<n-1)   //因为在n-2时候就已经判断了n-1所以只用到n-2
    {
        if(i==-1||p[i]==p[j])  //是否跳到开头或者匹配成功
        {
            ++i;
            ++j;
            ne[j]=i;
        }
        else
            i=ne[i];
    }
}
void kmp()
{
    int i=0,j=0;
    while((i<m)&&(j<n))
    {
        if(j==-1||s[i]==p[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j=ne[j];
        }
    }
    if(j>=n)  //判断是否全部匹配
        printf("%d\n",i-j+1);
    else
        printf("-1\n");
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&m,&n);
        for(int a=0;a<m;a++)
            scanf("%d",&s[a]);
        for(int a=0;a<n;a++)
            scanf("%d",&p[a]);
        getnext();
        kmp();
    }
    return 0;
}

在这些基础上的话有一些其它的扩展

1.最常见的问题

判断一个串在另一个串中出现了几次这个问题可以分成两种,一种是可以有折叠的,一种是没有重复的,比如问AZA在AZAZAZA中出现了几次,如果可以折叠的话就是3次,不可以折叠的话就是2次,那么只需要在原来的板子上面加一个操作就行

可以折叠的话就用用next数组往前跳,不可以折叠的话就是跳到开头继续判断就行了

可以折叠

if(j==-1||s[i]==p[j])
       {

          if(j==n-1)
           {
               ++k;
               j=ne[j];
               continue;
           }
           ++i;
           ++j;

       }

不可以折叠

if(j==-1||s[i]==p[j])
       {

          if(j==n-1)
           {
               ++k;
               j=0;
               ++i;
               continue;
           }
           ++i;
           ++j;

       }

2.循环节

判断循环节的问题,首先这个问题在上面getnext()中得到next数组的时候while里面的是j<n而不是n-1,因为如果n-1的话是从n-2那个环节判断的n-1,但是判断的是n-1前面的,没有n-1本身,而j<n的话判断了n-1,然后把n-1的情况存到next[n]中

然后的话就是 len§-next[n]代表的是循环节的长度,如果len§% ( len§-next[n])==0,就表明是一个完整的循环节,否则就是有残缺的,如果是完整的循环节,那么他的循环次数就是 len§ / (len§-next[n])

3.判断一个字符串的前缀和另一个字符串的后缀的最长公共部分

比如abcd的前缀和dabc的后缀的最长公共部分就是abc
让这两个字符串先结合成一个串,然后对这个串进行next处理,中间不用加限制条件,等到最后判断的时候直接判断,用到strcat函数strcat(a,b)就是把数组b连接到a的末尾

#include <iostream>
#include<algorithm>
#include<string.h>
#include<stdio.h>
using namespace std;
char s[100005],p[50005];
int m,n;
int ne[100005];
void getnext()
{
求next[]数组的略去了
    int h=ne[l]; 
    while(h>m||h>n)
    //如果s与p连接后ne[l]>m或者n的话就表明p开头匹配next的值连着s结尾的开始匹配了,所以肯定要往前面返回,就是h=ne[h]
    {
        h=ne[h];
        //h就是前缀和后缀的最长长度
    }
    if(h==0)
    {
        printf("0\n");
    }
    else
    {
        for(int a=0;a<h;a++)
        {
            printf("%c",s[a]);
        }
        printf(" %d\n",h);
    }

}
int main()
{
    while(scanf("%s",s)!=EOF)
    {
        scanf("%s",p);
         m=strlen(s),n=strlen(p);
        strcat(s,p);
        getnext();
    }
}

4.暴力匹配

比如告诉你10个长度为60的字符串,然后问你他们这些串相同的最长公共子串,这时候你需要用到暴力的方法,可以把第一个当成母串,然后每次递增截取,第一次可以从60个字符中先找一个,然后将这一个和其它九个进行匹配,匹配的时候用strstr函数,如果有一个找不到的话就break结束暴力,然后截取60个字符中的另外1个字符,然后和其他9个匹配,如果第一个字符串的60个字符全都没找到的话直接结束最外层循环,因为1个字符都找不到的话那两个就更不用说了,然后直到匹配结束
strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。

5.扩展KMP

扩展kmp求出的next[i],extend[i]数组,代表的意思是 next[i]表示的是字符串T对自己进行操作,next[i]表示T串中从i这个位置开始(即T[i]开始到T串的结尾)与T(从0这个位置开始)本身的最长公共长度(即T[i ~n]与T[0 ~n]的最长公共前缀),extend[i]求出的是S串从i开始到结尾与T串的公共前缀长度(即S[i ~m]与T[0 ~n]的最长公共长度),解决的问题有
1.一个字符串所有的前缀子串在这个串中出现的次数之和(也可以用dp,dp[i]=dp[next[i] ]+1),比如abab的话就是a出现的次数+ab出现次数+abc出现次数+abcd出现次数=2+2+1+1=6 ,用next数组扩展自己然后将next数组求和就行了,至于什么原理我也不是太清楚
代码如下

int next[N],extand[N];
void getnext(char *T){// next[i]: 以第i位置开始的子串 与 T的公共前缀 
     int i,length = strlen(T);
     next[0] = length;
     for(i = 0;i<length-1 && T[i]==T[i+1]; i++);
          next[1] = i;
          int a = 1;
          for(int k = 2; k < length; k++){
                  int p = a+next[a]-1, L = next[k-a];
                  if( (k-1)+L >= p ){
                       int j = (p-k+1)>0? (p-k+1) : 0; 
                       while(k+j<length && T[k+j]==T[j]) j++;// 枚举(p+1,length) 与(p-k+1,length) 区间比较 
                       next[k] = j, a = k;
                  } 
                  else next[k] = L;
         } 
}
void getextand(char *S,char *T){
   memset(next,0,sizeof(next)); 
         getnext(T); 
         int Slen = strlen(S), Tlen = strlen(T), a = 0;
         int MinLen = Slen>Tlen?Tlen:Slen;
         while(a<MinLen && S[a]==T[a]) a++;
         extand[0] = a, a = 0;
         for(int k = 1; k < Slen; k++){
              int p = a+extand[a]-1, L = next[k-a];  
              if( (k-1)+L >= p ){
                   int j = (p-k+1)>0? (p-k+1) : 0; 
                   while(k+j<Slen && j<Tlen && S[k+j]==T[j] ) j++; 
                   extand[k] = j;a = k; 
              } 
              else extand[k] = L;         
         }
}

Manacher算法

这类算法是求回文串而设计出来的算法,回文串就是正着读和倒着读都一样的,最常见的问题就是问你一个字符串中最长的回文串长度
模板如下:

#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<string.h>
#define mod 10007
using namespace std;
char s1[110010],s2[220010]; //s1是初始的串,s2是变换过后的串
int p[220010];
int  manacher()
{
    int k=0;
    s2[k++]='$';
    s2[k++]='#';
    int n=strlen(s1);
    for(int a=0;a<n;a++)
    {
        s2[k++]=s1[a];
        s2[k++]='#';
    }
    s2[k]='\0';
    int i=0,mx=0,ans=-1;
    p[0]=0;
    for(int a=1;a<k;a++)
    {
        if(a<mx)
        {
            p[a]=min(p[2*i-a],mx-a);
        }
        else
            p[a]=1;
        while(s2[a-p[a]]==s2[a+p[a]]) //因为第一次判断肯定成功,所以最后ans-1才是最长字符串长度
            p[a]++;
        if(a+p[a]>mx)
        {
            i=a;
            mx=a+p[a];
        }
        ans=max(ans,p[a]);
    }
    return ans-1; //ans-1为字符串长度而不是ans
}
int main()
{
    while(scanf("%s",s1)!=EOF)
    {
         printf("%d\n",manacher());
    }
    return 0;
}

如果题目让你求最长串的起始位置的话,假设l为左位置,r为右位置的话,(l和r指的是在原串s1的位置),然后就可以在原串中输出来

    r=(p[f]-1)/2+f/2-1; // p[f]就是上面模板的ans,p[f]-1就是最长回文串的长度,而f就是以f位置的那个字符为中心的回文串往两边扩展得到最长回文串
    l=r-p[f]+2;

如果遇到这类问题,给你一串字符串,让你把它分成两串,每一串的每一个字母都有它自己的价值,如果分开后的串是回文串,他的价值就是每一个字母价值之和,否则价值为0,让你求最大的价值(HDU 3613)

//前面的正常操作就行,我只写核心的代码
//n是字符串的长度,ans是最大价值,sum[n]指的是前n个字符串的价值(先不考虑是不是回文串)
  int ans=0;
for(int a=0;a<n-1;a++)
        {
            int num=0;
            int k1=a+1,k2=n-a-1; //把字符串分成两段后,k1是字符串的左部分,k2是右部分
            int k=p[a+2]-1;
            if(k1==k)//判断前串是不是回文串
            {
                num+=sum[a];
            }
            k=p[a+n+2]-1;
            if(k2==k) //判断后串是不是回文串
            {

                num+=(sum[n-1]-sum[a]); 
            }
            ans=max(ans,num);
        }

最小/(大)表示法

可以通过此算法将字符串按照字典序最小(大)排序,最后求出的min(i,j)就是表示从这个位置开始的串字典序最小(大)

int Get_min()
{
    int n = strlen(s);
    int i = 0,j = 1,k = 0,t;
    //表示从i开始k长度和从j开始k长度的字符串相同
    while(i < n && j < n && k < n)
    {
        t = s[(i+k)%n] - s[(j+k)%n];
        //t用来计算相对应位置上那个字典序较大
        if(!t) k++;//字符相等的情况
        else
        {
            if(t > 0) i += k+1;//i位置大,最大表示法: j += k+1
            else j += k+1;//j位置大,最大表示法: i += k+1
            if(i == j) j++;
            k = 0;
        }
    }
    return i >j ?j :i;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值