KMP 字符串

KMP这道题,有一个非常重要且烧脑的数组:next[ ]数组,一旦将这个数组的含义理解透彻了,那么KMP类似的题也就能迎刃而解了。

先将介绍一下待会会用到的几个定义。字符串前缀与后缀。
举个例子:以字符串abcaba来解释

从第一个字母向后枚举时,此过程的前缀与后缀分别为:
"a": 前缀:空集,后缀:空集                               "ab": 前缀:a,后缀:b
"abc":前缀:a,ab;后缀:c,bc                        "abca":前缀:a,ab,abc 后缀:a,ca,bca
"abcab":前缀:a,ab,abc,abca;后缀:b,ab,cab,bcab
"abcaba":前缀:a,ab,abc,abca,abcab;后缀:a,ba,aba,caba,bcaba
那么我们可以发现对于字符串abcaba,前缀与后缀有相等的请况,我们将其记录下来
"a": 前缀:空集,后缀:空集     无相等 ;ne数组不变                         "ab": 前缀:a,后缀:b   无相等   ;ne数组不变             "abc":前缀:a,ab;后缀:c,bc    无相等;ne数组不变                "abca":前缀:a,ab,abc 后缀:a,ca,bca        相等"a",长度为1;ne[4] = 1
"abcab":前缀:a,ab,abc,abca;后缀:b,ab,cab,bcab  相等"ab",长度为2;ne[5] = 2
"abcaba":前缀:a,ab,abc,abca,abcab;后缀:a,ba,aba,caba,bcaba     相等"a",长度为1; ne[6] = 1
在这里引出的前缀与后缀 是next[ ]数组的核心
接下来这张图较为抽象,可先查看后续的模拟,会更直观。看完模拟再来查看此图会更清晰          

假设有一母串(红线),与一子串(紫线)。
当一开始匹配时,母串的j ~ j1与字串的i0 ~ i0+n-1匹配,而继续向下遍历时,最后一个字符i0 + n不与母串的j1 +1匹配,那么造成匹配失败。
字串向后移动,以寻求匹配,当移动到蓝线时,此时匹配成功,我们可以发现:
上述三条线的黑色部分的字符串相等,又因为子串自己本身的①与②的字符串相等,那么②与③的字符串相等。
由此可见,倘若我们能通过某种标记使得在第一次匹配不成功时,能立马让字串的前缀②与母串的③进行匹配上,那么我们只需要继续匹配字串的蓝线部分即可,省去了大量的时间来匹配①的字符串。
注意了!
我们此时仔细观察字串的第一种状态(紫线),②与①相等(自己本身的前缀),而②与紫线的黑色部分(后缀)相等,那么我们就可以通过前后缀相等的方式,在第一次匹配不成功时,让第一种状态(紫线)直接跳转到第二种状态(蓝线),进而省去了匹配黑色部分字符串的时间,只需匹配剩余蓝线的字符串即可。

举个具体的例子:假如子串为上述"abcaba",母串为"abdabcabd",那么我们在进行匹配时:
 

 题目:831. KMP字符串 - AcWing题库

代码:

/*
kmp
1、先想一下暴力做法,2、如何去优化
1、for(循环母串)
    for(循环字串)
*/
#include<bits/stdc++.h>

using namespace std;

int n,m;
//s表示子串,p表示母串
char s[100010],p[1000010];
//ne[]数组是记录的子串的前后缀相等的情况
int ne[100010];

int main()
{
    cin >> n >> s+1 >> m >> p+1;
    
    //寻找子串前后缀相等的情况
    for(int i = 2, j = 0; i <= n; i++)
    {
        //当子串的前后缀出现了不相等的情况,那么就查看下一组是否相等如果都不想等,那么j = 0,退出while循环
        while(s[i] != s[j + 1] && j) j = ne[j];
        //如果相等,那么就向后移动
        /*
        比如aba,一开始的s[2]!=s[0+1],由于j = 0,所以不会进入while循环,也不会进入if语句
        此时记录下ne[2] = 0;
        i++;
        因为此时s[3] == s[0+1],仍不会进入while循环,但是会进入if循环,使得j++;
        此时记录下ne[3] = 1,接着遍历结束,得到ne[3] = 1;
        那么如果是abc的话,遍历结束,ne数组整体都不会改变值,仍为0
        */
        if(s[i] == s[j + 1]) j++;
        //记录下当前的ne[i]值
        ne[i] = j;
    }
    
    for(int i = 1,j = 0; i <= m; i++)
    {
        /*
        比如子串aba;母串ababa
        对于子串:ne[3] = 1;
        那么第一次匹配,输出0,第二次直接令j = ne[j] = 1,
        那么可以直接判断p[4]是否与s[1+1]相等,如果不相等,那么就继续后退j = ne[j](退而求其次)
        直到找到一组相等的p[i] == s[j+1],如果没找到,那么j=0跳出循环
        */
        
        //同样的,当母串与子串出现了不匹配,那么根据前后缀。找到下一组能直接进行匹配的字符串(退而求其次)
        while(p[i] != s[j+1] && j) j = ne[j];
        //如果相等,则j向后移动,判断下一个子串与母串的字符是否相等
        if(p[i] == s[j+1]) j ++;
        
        //如果已经全部匹配完成
        if(j == n)
        {
            //输出当前匹配在母串的起始下标
            cout << i - n << " ";
            //根据前后缀,找到下一组能直接进行匹配的字符串
            j = ne[j];
        }
    } 
}

哈希方式,看可以参考这一篇:哈希表(hash)

代码:

#include<iostream>

using namespace std;

const int N=1e6+10,P=131;
typedef unsigned long long ULL;

ULL hb[N],ha[N],p[N];

int get(ULL h[],int l,int r)
{
    //将高位的减去(这里是指进制下的高低位)
    //将字符串转为哈希方式下的公式
    return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
    int n;
    scanf("%d",&n);
    char a[N];
    scanf("%s",a+1);
    int m;
    scanf("%d",&m);
    char b[N];
    scanf("%s",b+1);
    p[0]=1;
    for(int i=1;i<=m;i++)
    {
        //预处理,将字符串a与字符串b转化成P进制下的数
        p[i]=p[i-1]*P;
        hb[i]=hb[i-1]*P+b[i];
        ha[i]=ha[i-1]*P+a[i];
    }
    
    for(int i=n;i<=m;i++)
    {
        //套用比较公式
        if(get(hb,i-n+1,i)==get(ha,1,n))
        printf("%d ",i-n);
    }
    return 0;
}

KMP这一节我是放到最后才去听,现在前四章都已经学完,明天准备开dp,所以开dp之前把先前所遗留下的问题弥补起来。因为是放到最后才去听,所以学起来没那那么难,主要还是得益于最后一节的哈希匹配。能用KMP求解的题,用哈希一般都能做出来,所以当时听完了哈希,KMP这道题用哈希就立马做了出来。当时的难点也许,也许在现在看来就没有那么难了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值