zoukankan      html  css  js  c++  java
  • [算法讲解] KMP & EXKMP : next与extend的羁绊

    KMP

    KMP作为一个广为人知的字符串匹配算法——也是本文的前一半。

    旨在着重讲解next数组的求法,并使读者理解。

    先扔代码 luoguP3375 【模板】KMP字符串匹配

    #include<iostream>
    #include<cstring>
    using namespace std;
    const int N=5000002;
    int next[N];
    string s1,s2;
    void init(string s){
      for(int i=0,j=next[0]=-1,len=s.size();i<len;){
        if(j==-1||s[i]==s[j])
            next[++i]=++j;
        else j=next[j];
      }
    }
    int main(){
        cin >> s1 >> s2;
        init(s2);
        for(int i=0,j=0,len=s1.size();i<len;){
            if(j==-1||s1[i]==s2[j])
                ++i,++j;
            else j=next[j];
            if(j==s2.size())cout << i-s2.size()+1 << endl;
        }for(int i=1,len=s2.size();i<=len;++i)
            cout << next[i] << " ";
        return 0;
    }

    先说两个概念:

     真前缀和真后缀

    真前/后缀  :  即不包括原字符串的前/后缀。

    大致说一下

    kmp的过程

    1)有两个指针i ,j分别指向文本串T和模式串P , 如下图,长的是T,短的是P。

      初值i = j = 0;

     

    2)比较 i=0,j=0,发现不等,此时把i后移一位,相当于把模式串右移一位,比较i=1 j=0。

    3) ....一路比较,直到i=3,j=0时才发现相等。 ++i,++j。

    4) i=5 , j=1时发现仍然相等.....

    5)直到i=10,j=6时,发现不相等。

    6)根据暴力的思想,我们应当把i j回到之前,接着“逐位匹配”。像这样。

     

    7)但是本文不讲暴力!让我们回到这张图。

    我们其实已经知道了文本串中的后面一个 "AB" 与模式串中的前两位“AB”是相同的,所以为什么不直接把模式串多几格呢?

    但是我们怎么知道有两位"AB"相同?其实是因为在不匹配的"D"前面的"AB"匹配了,而这个"AB"与模式串首个“AB”相

    同,于是我们就可以这样跳跃。

    这个“如何跳跃”,就是next数组。后面详细讲解。

     

     8)如图,把模式串移动到这个位置,i=10,j=2,这样直接使得前面的AB匹配上了,于是比较i=10和j=2,发现不匹配。

    那么此时跳到哪里呢?发现在C之前的"AB"中,没有相同的真前后缀,于是只好从头开始了,j=0。

    9)比较,匹配不上,++i,j=next[j]=0;

    10)过程大致清楚了吧。

     

    11)最后发现了全部匹配 : j=p.size()且p[j]==t[i]。此时重新匹配:j = nest[j] = 0; 即从头。

     

    12) 继续匹配,最后结束。

    重头戏:next数组的理解

    我们先看到 init 初始化函数。

    void init(string s){
      for(int i=0,j=next[0]=-1,len=s.size();i<len;){
        if(j==-1||s[i]==s[j])
            next[++i]=++j;
        else j=next[j];
      }
    }

    当然写成while的也行。

    void init(string s)  //这里j变成了k ....
    {
        int i=0,k=next[0]=-1,len=s.size();
        while(i<len) 
            if(k==-1 || s[i]==s[k]) 
                next[++i]=++k;      
            else k=next[k];         
    }

    首先,next[i]= 字符串中0~i-1 部分的最长的真前缀等于真后缀的长度 + 1。

    我们一点点来解释"next[i]= 字符串中0~i-1 部分的最长的真前缀等于真后缀的长度 + 1。"

    1>    "0~i-1"是因为当i不匹配时,只看前面的区间的前后缀,而且若不匹配i并不会++(所以下次还是从当前位比较)。

           如果是0~i,那么移动后当前位的字符还是相同的,没有意义。

    (正确的)比如P : ABCABC

           T : ABCABD

    如果最后一个C不匹配了,说明AB匹配,应当把AB后的第一个C(指p[2])拿去匹配。

    即:

    ABCABD

    ABCABC

    ->

    ABCABD

           ABCABC

    2>    "最长的真前缀等于真后缀的长度 + 1"

        显然要最长

      +1?因为其实下一步需要比较的还是相同前后缀的下一位(别忘了相同部分已经比较过了,要让他们对齐)。

    理解了思路之后就要理解代码。

    void init(string s){
      for(int i=0,j=next[0]=-1,len=s.size();i<len;){
        if(j==-1||s[i]==s[j])
            next[++i]=++j;
        else j=next[j];
      }
    }

    i,j表示当前处理的区间是0~i,判断的前缀和后缀长度为j,即前缀0~j-1,后缀 i-j+1~i

    (其实处理过程就是自己匹配自己)

    如果0~i前后缀匹配,则把next[i+1]=j+1,且i++,j++。否则就像匹配文本串时一样,跳到上一个匹配的地方。

    if(j==-1)用来特判越界。

    也许你会想,-1是什么,为什么不用0呢?

    我们先看看-1有什么用。

    next[0]=-1,使得更新的是next[++i]=++j=0。也就是没有相同前后缀,必须从头开始匹配。

    那么如果next[0]=0呢?显然不行,会使得跳跃时出现错误结果。

    j=0同理。

    那么kmp就讲完了(什么辣鸡结尾)。


    EXKMP

     前言

    mmp 这个 辣鸡 玩意儿花了我好久才理解,所以我必须得把它给讲清楚了。

    不过很少考。(其实我是因为考了才学的)

    模版题 luoguP5410 【模板】扩展 KMP

    概念

    EXKMP(扩展KMP)用于解决以下问题:

    > 给你两个字符串s,t,长度分别为n,m。

    > 请输出s的每一个后缀与t的最长公共前缀。

    (谁这么无聊出这种题啊)

    例题

    luoguP5410 【模板】扩展 KMP

    同样扔代码。

    #include<iostream>
    #include<cstring>
    #include<cmath>
    using namespace std;
    const int N=5000002;
    int next[N],extend[N];
    void init(string s){
        int now=0,ls=next[0]=s.size(),idx=0,far;
        while(s[now]==s[now+1]&&now+1<ls)++now;
        next[1]=far=now; idx=1;
        for(int i=2;i<ls;++i){
            if(i+next[i-idx]<far)
                next[i]=next[i-idx];
            else {
                next[i]=max(0,far-i);
                while(i+next[i]<ls&&s[i+next[i]]==s[next[i]])
                    ++next[i];
                if(i+next[i]>far)far=i+next[i],idx=i; 
            }
        }
    }
    void exkmp(string t,string p){
        init(p); // getnext
        int now=0,lt=t.size(),idx=0,far;
        while(p[now]==t[now]&&now<lt)++now;
        extend[0]=far=now;
        for(int i=0;i<lt;++i){
            if(i+next[i-idx]<far)
                extend[i]=next[i-idx];
            else {
                extend[i]=max(0,far-i);
                while(i+extend[i]<lt&&t[i+extend[i]]==p[extend[i]])
                    ++extend[i];
                 // attention about 0 for starter
                if(i+extend[i]>far)far=i+extend[i],idx=i; 
            }
        }
    }
    string s,t;
    int main(){
        cin>>s>>t;
        exkmp(s,t);
        for(int i=0,len=t.size();i<len;i++)printf("%d ",next[i]);
        puts("");
        for(int i=0,len=s.size();i<len;i++)printf("%d ",extend[i]);
        return 0;
    }

    首先有(请时刻牢记这个定义,最好用纸笔记一下)

    int next[N],extend[N];
    string s,t;                           //均从0开始编号
    //next[i]表示 s[i...n]与s的最长公共前缀的长度。
    //extend[i]表示 s[i...n]与t的最长公共前缀的长度。

    先说extend , (next : 怎么又是我最后)

     extend

     特殊情况

    1) 上面是s,下面是t (t是移动的那个)。绿色为已经匹配上的部分,红色框是失配的地方。下图为第一次匹配,暴力进行。

     2)看第二步。把t往后移了一位,于是变成了找s[1...n]和t的公共前缀。

      红线划掉的地方已经不关我们的事了。

    但我们发现此时做了大量的无用功,因为中间有很大的重叠部分。

    让我们回到1)处。

    由1)得,s[0...5] == t[0...5] 

    ∴ s[1...5] == t[1...5] 

    此时我们求的是s[1...5]与t的匹配。

    于是这个问题就变成了 (s[1...5]=) t[1...5] 与 t的匹配。

    还记得那个定义吗? 这就是next数组。 

    当然一般情况并没有那么简单。

    一般情况

    假设当前已经处理好了 extend [0...i)  ,也就是正在处理 s[i]  。

    记:

    1> farthest = max { j + extend[j] | 0 <= j < i} (已经到达的最远位置,即满足0 <= j < i 的 最大的 j+extend[j])

     为了方便,farthest 我们用far代替。

    2> idx(index) ,使得 j + extend[j]最大的 j。 也就是对应far 的 j。

      其实就是这样:

    int idx,far=0;
    for(int j=0;j<i;++j)
        if(far<j+extend[j])
            far=j+extend[j],idx=j;

    显然,其实是可以不记录far的,也有这样的写法,但是加加减减的不利于阅读和理解(把far都用idx+extend[idx]代替),所以本文.....就这么讲解。(雾)

    现在让我们把t挪一挪,使t[0]对准idx。

    pos,i对应的位置,pos = i-idx。

    下面就开始分类咯。

    一 、 K+next[pos] < far

    如图(这张图稍微调整了一下,希望不影响观看),绿线蓝线是相等的(雾),也就是说他们表示的区间相等。

    他们的长度都等于next[pos]。(看看定义)

    再看黄线蓝线

    我们有  s[idx...far] = t[0 ... far-index](因为index已经匹配过了)

    所以s[i ... i+next[pos]]  =  t[pos ... pos+next[pot]] 也就是黄线蓝线相等(既然整个都相等那么里面的每一个对应的也相等)

    所以绿线蓝线黄线

    咱们把t再挪回来。

    别光看这诡异的图啊.....绿线黄线是相等的....

    但是黄线 = extend[i]

    绿线 = next[i-idx]

    所以此时有:

    if(i+next[i-idx]<far)   
    
        extend[i]=next[i-idx]; 

    不过这里可能有人要问,(因为含在[idx,far]内)黄线蓝线的后一位不是也相等吗?为什么不会导致错误呢?

    回到问题的实质上来,我们要求的是extend[i],也就是比较黄线绿线,倘若绿线的下一位也与他们相等,则必然next[pos]就会+1,就不是这个值了,所以仍是正确结果。

    不过由此我们也发现,如果黄线末端是far位置,则不知道下一位与蓝线下一位的关系了,这时便不能这样处理,在下一类中。(看看这部分的标题,只有<号)

    二、K+next[pos]>=far

    这种也很好理解啦,我们只取等同于前面一种的一定相同的部分,后面的再暴力比较不就完事儿了吗?

    R只是一个讲解的时候稍微简化一下用的,代码用不着,R=pos+next[pos] 。

    R在s中对应的是i+next[pos] , 它 >= far。

    X代表超过far的长度。X=i+next[pos]-far+1。

    他使得 i+next[pos]-X=far-1。

    这样在三条线都去掉X部分的情况下即可化归为上一种情况。

    但是还要判断后面的字符,于是从far开始,直接暴力比较。

    but  , 您又发现,far不一定会 > i 呀 。得,没事,那就从头暴力吧。

    void exkmp(string t,string p){
        init(p); // getnext
        int now=0,lt=t.size(),idx=0,far;
        while(p[now]==t[now]&&now<lt)++now;
        extend[0]=far=now;
        for(int i=0;i<lt;++i){
            if(i+next[i-idx]<far)
                extend[i]=next[i-idx];
            else {
                extend[i]=max(0,far-i);
                while(i+extend[i]<lt&&t[i+extend[i]]==p[extend[i]])
                    ++extend[i];
                if(i+extend[i]>far)far=i+extend[i],idx=i; 
            }
        }
    }

    第一遍因为没有far所以直接暴力。

        第一次暴力用剩下的now与后面的now一点关系都没有,相当于是两个变量!并不是要接着上一次的now什么的。

    当然,要注意暴力时判出界,最后还要更新idx和far。

    那么extend讲完了。

    下面讲next。

    next

     next的意义和extend很像,所以做法几乎是一样的。

    不过next[0]显然就等于原串长,next[1]直接暴力,所以从next[2]开始。

    void init(string s){
        int now=0,ls=next[0]=s.size(),idx=0,far;
        while(s[now]==s[now+1]&&now+1<ls)++now;
        next[1]=far=now; idx=1;
        for(int i=2;i<ls;++i){
            if(i+next[i-idx]<far)
                next[i]=next[i-idx];
            else {
                next[i]=max(0,far-i);
                while(i+next[i]<ls&&s[i+next[i]]==s[next[i]])
                    ++next[i];
                if(i+next[i]>far)far=i+next[i],idx=i; 
            }
        }
    }

    ls?注意,本文不讲解任何树!ls是len_of_string !

    这里注意虽然next[0]很长,但是idx不能为0,far也不能为原串长!否则next[i]=next[i-idx]等于没用。(哪有白嫖的好事)

    其它的就是把extend 都改成next , 字符串名也改一下罢了。

    完结撒花!(雾)

    Z算法(Z-algorithm)

    Z算法就是把next和extend合并成同一个数组z,可以简化一下代码。因为窝太弱了还不会所以就不写了。

    END~

  • 相关阅读:
    CSS3 3D转换
    CSS3 2D转换
    CSS3 字体
    CSS3 文本效果
    Spring核心技术(十三)——环境的抽象
    表达式求值
    一些设计上的原则
    POJ2503字典树
    MBR结构解析与fdisk的bash实现
    微服务指南走北(三):Restful API 设计简述
  • 原文地址:https://www.cnblogs.com/lsy263/p/11516623.html
Copyright © 2011-2022 走看看