K M P 超简单的理解

U1S1,学了那么久的KMP至今不理解,

每自往思,三顾谷歌百度之中,咨询以当前之急,似懂非懂,遂许粘贴以瞎改。

后值反思,亡羊补牢。

然后凭着以前学过用过的基础,深度复习一遍,想不到竟如此简单

也就几个步骤就可以理解了,

假设:

母串为A,使用遍历变量i,子串为B,使用遍历变量j

1. 以暴力匹配为基础

2. 清楚暴力匹配在每次失配的时候,都是i+1,j=0从头开始匹配的操作

3. 然后优化掉j=0的操作,延伸出了(子串的)最长前后缀和next数组(带有证明)

4.再优化掉i+1的操作,延伸出了一系列证明发现——每次匹配到例如A[i+k]==B[j+k],然后A[i+k+1]!=B[j+k+1]也就是刚好下一位失配的时候,保证此时的母串匹配到的位置i+k+1不变

(也就是,在暴力匹配思维中被优化掉的i=i+1,等效变成i=i+k+1

结束!!!

-------------------------------------分割线--------------------------------------------------下面来分步解释3,4步骤。

3.1最长前后缀

有证明:B[i-k~i-1]==B[j-k~j-1] 

也就是,B串的第i个的  和  B串第j的  前k个字母相同

 

在这个“ABCDABD”串中,i,j,k分别表示的值就是3,6,2

 

肉眼看出来思考一下可以怎么利用呢?

也就是当子串B匹配母串A已经匹配到B串的 j 这个位置的时候,发现不能匹配,

只需要移动到B i 这个位置去与母串重新匹配即可!!!

(这里就替换掉了之前所说的j=0的操作)

当发现母串的C与子串D不匹配的时候,直接将子串B按上述操作去重新匹配,得到一个步所示的匹配位置。

这里也就涉及到实际匹配过程中的证明:A[i-k~i-1]==B[j-k~j-1] 

也就是,A串的第i个的  和  B串第j的  前k个字母相同

咱们这就没必要去多搞证明了,觉得自己脑子还行的,可以看最后我推荐的博客里的证明。

-----------------小分割线-----------------

3.2 next数组

如上,咱们已经发现了,子串B的一些东西有助于优化,那么这个东西就是由  最长前后缀  推出来的 next数组

细看公式:A[i-k~i-1]==B[j-k~j-1] ,这个“-1”是否有些碍事?

那么最长前后缀得到的一串数字:

 处理成,整体向右移一位,首项设为-1,则就变成了如下:

首先说明next数组的定义——next[i]的值 表示,在 i 这个位置的最长前后缀的长度值

那么,这样一来,就恰好可以满足:上述所说的  替换掉  暴力思维里的 j=0 的操作  的操作 就是  j=next[j]

这里要深度思考理解一下,就按照上面那个失配图的那里,代码逻辑思考一下j的变换过程

(实际等效于子串向右移了j-next[j]位,这个右移不需要记忆,理解就行)

(还是解释一下吧,那里的 j=5 , next[5]=1,也就是实际右移了5-1位,4位

如此理解了之后,再来说如何求得next数组的问题

这里暂时就不解释了,想知道的也可以跳转一下到我推荐的博客里去看,这里就贴上我写的标码吧

 

void Getnext(char s[])
{
    n[0]=0;
    for(int i=1,k=0;s[i];++i)
    {
        while(k&&s[i]!=s[k])k=n[k-1];
        n[i+1]=s[i]==s[k]?++k:0;
    }
}

 

还不理解的,可以按上述样例输出一下得到的n数组

那么,步骤3就过了

--------------------------------------------------------------分割线-------------------------------------------------------------------

4.优化i+1的操作,这一步,,唔,,,简单思考一下糊弄自己过去就行,

强行理解运用了next数组子串去匹配的匹配,i变为i+1是多余的就行,i只管++,其他都是子串的匹配变量变化

当然,真想深度理解的,也可以跳去看我最后推荐的博客吧。

那么最后就贴一下这替换i+1的实际匹配代码吧,因为本身代码不固定的原因,就找一个例题来作为代码了

http://120.78.128.11/Problem.jsp?pid=2149(福工院fjut的2149题)

 

for(i=0,k=0;a[i];++i)
{
while(k&&b[k]!=a[i])k=n[k-1]; if(b[k]==a[i])++k; if(k==lb) { printf("%d\n",i-lb+1); break; } } if(i==la)puts("-1");

 

完整代码:

#include<cstdio>
#include<cstring> 
#define N 1000009
char a[N],b[N];
int n[N];
/*
等效移动k-next[k]
p[i]!=p[j]
p[i-k`i-1]=p[j-k`j-1] 
也即,next数组表示失配时下一个可匹配的位置, 
*/
void Gn(char s[])
{
    n[0]=0;
    for(int i=1,k=0;s[i];++i)
    {
        while(k&&s[i]!=s[k])k=n[k-1];
        n[i+1]=s[i]==s[k]?++k:0;
    }
}
int main()
{
    while(~scanf("%s %s",a,b))
    {
        Gn(b);
        int la=strlen(a),lb=strlen(b),i,k;
        for(i=0,k=0;a[i];++i)
        {
            while(k&&b[k]!=a[i])k=n[k-1];
            if(b[k]==a[i])++k;
            if(k==lb)
            {
                printf("%d\n",i-lb+1);
                break;
            }
        }
        if(i==la)puts("-1");
    }
    return 0;
}

更新代码,推荐使用这个封装好了的code:

 

class Solution
{
    vector<int> next;
    int x, y;
    void getnext(string a)
    {
        next.emplace_back(0);
        for (int i = 1, k = 0; i < y; ++i)
        {
            while (k && a[i] != a[k])
                k = next[k - 1];
            next.emplace_back(a[i] == a[k] ? ++k : 0);
        }
    }

public:
    int strStr(string haystack, string needle)
    {
        x = haystack.length(), y = needle.length();
        if (y == 0)
            return 0;
        next.clear();
        getnext(needle);
        for (int i = 0, k = 0; i < x; i++)
        {
            while (k && haystack[i] != needle[k])
                k = next[k - 1];
            if (haystack[i] == needle[k])
                ++k;
            if (k == y)
                return i - y + 1;
        }
        return -1;
    }
};
母串haystack找子串needle

 

KMP,结束!!!

推荐博客:https://blog.csdn.net/v_july_v/article/details/7041827(图也是这里来的)

 总结一下KMP的用法(也体现next数组相应的性质or作用):

1. 基本的string.find

2. 字符串中的前缀次数和:http://120.78.128.11/Problem.jsp?pid=1303 

(解法是,ans=遍历 next数组 非零k=next[k]递归次数 +字符串长度)

3. 字符串中的相同前后缀:http://120.78.128.11/Problem.jsp?pid=2150

(解法是:从末尾失配跳转一遍next数组即可)

4. 字符串中的循环周期和周期次数:http://120.78.128.11/Problem.jsp?pid=1304

解法是:遍历一遍next数组,找到i%(i-next[i])==0的位置,

这个位置即满足循环周期,长度为i,循环次数为i/(i-next[i])

在next数组中有个性质是,next数组的值,有i-next[i]表示失配时,子串右移的位数

那么,当位移位数和长度成倍数关系时候,这个子串就是循环串了

5. 两串最长前后缀匹配:http://120.78.128.11/Problem.jsp?pid=1305

(深度理解next数组,不多解释就贴一份带分析的ac代码吧)

 

#include<iostream>
#include<algorithm>
using namespace std;
#define N 202100
void cs(string x){cout<<x<<endl;}
string a,b;
int n[N*2];
int Gn(string s)
{
    n[0]=-1;
    int k=0,l=min(a.length(),b.length());
    for(int i=1;s[i];++i)
    {
        while(k&&s[i]!=s[k])k=n[k];
        n[i+1]=s[i]==s[k]?++k:0;
    }
    return min(l,k);
}
/*
分析:
        考虑最短连接的字母串
        有a+b和b+a两种情况,
        也就是比较 
        a串末尾与b串开始的匹配长度 
        与
        b串末尾与a串开始的匹配长度 
        
        那么如何找这个匹配呢?
        next数组前身是最长前后缀,那么也就是说
        a+b串后去找的字母串的next数组应该是b+a的最长匹配长度
        b+a串后去找的字母串的next数组应该是a+b的最长匹配长度 
        
        有两点需要注意: 
        因为要求是首尾连接,所以取值必须是next数组最后一位值 
        因为next数组求值本身是两字符串的匹配,所以next数组取值可能超过a,b串的本身长度,所以要取最小值 
*/ 
int main()
{
    while(cin>>a>>b) 
    {
        int la=Gn(a+b),lb=Gn(b+a);
        if(lb>la)cs(a+string(b,lb));
        else if(la>lb)cs(b+string(a,la));
        else
        {
            string A=a+string(b,lb);
            string B=b+string(a,la);
            cs(A>B?B:A);
        }
    }
    return 0;
}
一看就懂

 6. 字符串中的所有循环节:https://vjudge.net/problem/FZU-1901

(分析见代码)

#include<iostream>
#include<cstring>
#include<cstdlib>
#include<vector> 
#include<queue>
#include<cmath>
#include<map>
#include<algorithm>
using namespace std;
#define N 2021
#define mt(x) memset(x,0,sizeof x)
typedef long long ll;
void cn(ll x){cout<<x<<endl;}
void cs(string x){cout<<x<<endl;}
int n[N],ans[N];
string a;
void Gn(string s)
{
    n[0]=-1;
    for(int i=1,k=0;s[i];++i)
    {
        while(k&&s[i]!=s[k])k=n[k];
        n[i+1]=s[i]==s[k]?++k:0;
    }
}
/*
4
ooo
acmacmacmacmacma
fzufzufzuf
stostootssto
*/
void solve()
{
    int t;
    cin>>t;
    for(int i=1;i<=t;++i)
    {
        string s;
        cin>>s;
        Gn(s);
        /*
        题意:找出所有p,使得s[0~i]==s[0~i+p] 
        分析:找循环子串,
        next[i]=p表示字符串 s[0~p-1]==s[i~i+p-1] ,即匹配的长度p
        next从末尾值跳转k=n[k],k即为所有循环节的初始下标
        那么每次 累加 位移长度i-n[i]即可
        
        另外也有做法,遍历next,输出:字符串总长l - 每一次跳转值p 
        这个不太好理解,
        我的理解是,
        i-next[i]是表示当前串在位置i失配时候的下一次匹配的右移
        那么
        l-next[i]是表示当前串 从头 开始的 右移 
        */
        int l=s.length(),num=0;
        int p=l;
        while(p)
        {
            ans[num++]=p-n[p]+ans[num-1];
            p=n[p];
            //ans[num++]=l-p; 
        }
        printf("Case #%d: ",i);
        printf("%d\n",num);
        for(int j=0;j<num;++j)
            printf("%d%c",ans[j],j==num-1?'\n':' ');
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0); 
    solve();
    return 0;
}
View Code
posted @ 2021-06-06 22:29  Renhr  阅读(611)  评论(0)    收藏  举报