KMP学习笔记

本文详细介绍了KMP算法的原理,包括部分匹配值(next数组)的计算及其在字符串匹配中的应用。通过实例展示了KMP算法如何用于查找一个字符串在另一个字符串中首次出现的位置、重复覆盖的次数以及不可覆盖的次数。此外,还讨论了循环节问题,并提供了多个相关问题的解题思路和代码实现。
摘要由CSDN通过智能技术生成

原理

失配的时候直接移动模式串到失配的位置(即已经匹配的长度)来加快速度,但是可能会漏掉已经匹配的这部分里前后缀相同的情况,所以需要计算部分匹配值pm
PM=maxlen(前缀=后缀) 前缀不包括最后一个字符 后缀不包括第一个字符
由上可知如果当前位置失配 移动的位数为已经匹配的位数-当前这一位的pm
设当前位置为j 则位数即move=(j-1)-pm[j-1]
令pm右移一位 得到next数组 定义next[0]=-1 因为不可能用到
next[j]=pm[j-1] move=j-1-next[j] 移动之后j的位置即j=j-move=next[j]+1
此时令next=next+1 则当在j位置失配时 j=next[j]
此时next数组的含义恰好为已匹配的字符串的部分匹配值

next数组的计算

以上是next数组的定义推导和手工计算方法 代码采用另外一种更快的方法
将待求的字符串(即模式串)错开一位进行自身的匹配 令上面的指针为i=0下面的指针为j=-1 则它们匹配的长度正好是最大的前后缀的长度 甚至刚好符合前后缀分别不包括最后一个和第一个的定义 用下面一个串的指针j表示正好就是next值
在发生失配的时候 由于i是从0开始的 所以next[j]一定是已经计算过的 可以直接用j=next[j] 代码如下

void getNext(char *str)
{
    int i=0,j=-1;
    int n=strlen(str);
    nxt[0]=-1;
    while(i<n)
    {
        if(j==-1||str[i]==str[j])
        {
            i++;j++;
            nxt[i]=j;
        }
        else
            j=nxt[j];
    }
}

练习

hdu1711

题意

下面一个数组在上面的数组第一次出现的位置

int kmp(int *s,int *t,int slen,int tlen)
{
    int i=0,j=0;
    while(i<slen&&j<tlen)
    {
        if(j==-1||s[i]==t[j])
        {
            i++;j++;
        }
        else
            j=nxt[j];
    }
    if(j==tlen)return i-j+1;
    return -1;
}

hdu1686

题意

上面的模式串在下面的子串中可覆盖的出现的次数
注意当找到一个匹配的之后假装在这个地方失配了 达到重复覆盖的计算次数的效果

int kmp_count()
{
    int ret=0;
    int i=0,j=0;
    int slen=strlen(s);
    int tlen=strlen(t);
    while(i<slen)
    {
        if(j==-1||s[i]==t[j])
        {
            i++;j++;
        }
        else
            j=nxt[j];
        if(j==tlen)
        {
            ret++;
            j=nxt[j];
        }
    }
    return ret;
}

hdu2087

题意

上面的模式串在下面的子串中不可覆盖的出现的次数
注意当找到一个匹配的之后的策略和上面就不一样了 就直接从头开始匹配

int kmp_count()
{
    int ret=0;
    int i=0,j=0;
    int slen=strlen(s);
    int tlen=strlen(t);
    while(i<slen)
    {
        if(j==-1||s[i]==t[j])
        {
            i++;j++;
        }
        else
            j=nxt[j];
        if(j==tlen)
        {
            ret++;
            j=0;
        }
    }
    return ret;
}

hdu3746 循环节问题

题意

一个字符串 问最少在左边或右边加多少字符使得字符串由大于一个循环节组成

题解

  1. 为什么可以只在右边加?因为题目提示里的环链和循环节的性质都告诉了我们左边和右边是一样的
  2. 这题和kmp有什么关系?首先我们要认识到 一个符合题意的字符串会有怎样的性质 至少第一个循环节和最后一个循环节是相等的 这其实就是说一个循环节长度的前缀等于后缀 再多想一下 这个字符串是由若干个循环节组成的 那么也就是说除了最后一个循环节的前缀 和 除了第一个循环节的后缀 他们俩是最大的部分匹配值 这就是next数组的定义
  3. 考虑极端情况 当最后一个位置的后一位next值为len-1的时候 即两个相同的串 上面不动 下面往右移了一位 此时中间对着的len-1位都是匹配的 此时有下面的第1个字符等于上面的第二个字符 下面的第二个字符等于上面的第三个字符… 而上面和下面其实都是一样的 所以就是说整个字符串全是一样的字符。考虑一般情况 由next数组的定义 对齐的长度为next[len](字符串坐标从0开始计数)
    推导过程
	int len=n;
    int lenB=nxt[len];
    int lenA=len-lenB;
    int x=lenB%lenA;
    if(x==0&&lenB!=0)
        ans=0;
    else
        ans=lenA-x;
    printf("%d\n",ans);

hdu1358 循环节问题

题意

输出一个字符串所有可循环的前缀的循环节长度和出现次数

题解

注意一点 虽然next数组是从0开始的 所以对于每一个位置i而言它的部分匹配值应该是nxt[i+1] 但是next数组的含义是已匹配的字符串的部分匹配值 所以对于下面的代码中的i实际就代表了当前处理的前缀长度 而不能考虑从0开始所以要+1

void slove(char* s,int n)
{
    int len,tim;//循环节的长度和循环节出现的次数
    for(int i=1;i<=n;i++)//tim>1所以n其实没有影响
    {
        len=i-nxt[i];
        tim=i/len;
        if(len!=i&&tim>1&&len*tim==i)
            printf("%d %d\n",i,tim);
    }
}

hust1010 循环节问题

题意

有一个字符串A,假设是”abcdefg”,由A可以重复组成AAA,从中截取一部分(至少包含一个以上完整A)为B。给出字符串B,求A最短的长度。

题解

第一个循环节问题的变形

void slove(int n)
{
    int len=n-nxt[n];//此处似乎存疑
    printf("%d\n",len);
}

poj2406 循环节问题

题意

循环节出现的次数

题解

        len=n-1-nxt[n-1];
        ans=n/len;
        if(ans*len!=n)ans=1;//这一行不能漏
        printf("%d\n",ans);

poj2752 前缀等于后缀的所有值

题意

rt

题解

next数组的理解 已匹配的字符串的部分匹配值 所以从最后一个位置开始的每个next值都是匹配值 另外注意next数组的计算是先更新i、j再更新next值

void slove(char *s,int n)
{
    vector<int>ans;
    int t=n;
    while(nxt[t])
    {
        ans.push_back(nxt[t]);
        t=nxt[t];
    }
    int l=ans.size();
    for(int i=l-1;i>=0;i--)
    {
        printf("%d ",ans[i]);
    }
    printf("%d\n",n);
}

poj3080 简单应用

题意

10个长度为60的字符串的最长公共子串

题解

由于题目数据范围的限制 所以直接枚举就可以了 码力下降的太厉害了 代码量上一点就不敢动了 对于不确定的细节不要想着莽4

        ans="";
        for(int i=0;i<60;i++)
        {
            int now=max(3,(int)ans.size());
            for(int j=now;i+j<=60;j++)
            {
                string tt=s[0].substr(i,j);
                getNext(tt,j);
                bool flag=false;
                for(int k=0;k<n;k++)
                {
                    if(!kmp(s[k],tt))
                    {
                        flag=true;
                        break;
                    }
                }
                if(!flag)
                {
                    if((int)ans.size()<j)
                        ans=tt;
                    else if((int)ans.size()==j)
                        ans=ans<tt?ans:tt;
                }
            }
        }
        if(ans=="")
            ans="no significant commonalities";
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值