KMP——从入门到不会打题

KMP——从入门到不会打题


前言

  如果你不了解哈希,建议先观看本蒟蒻的另一篇博客,对哈希有一定的理解   哈希大法吼

 

  KMP算法,别名烤馍片或者看毛片,由烤馍片男子天团三位神犇同时发现的一种强大的单模式串匹配算法

  通俗翻译即寻找一个模式串是否在一个文本串中出现过,出现过几次,出现的位置等等。

  用于更快速地将口吐芬芳的用户禁言

 


一般算法解决问题

  首先我们分析一般的单模式串匹配算法:

  1.暴力枚举法:

    每次依次匹配两个字符串的每一位,这样如果是aaaaaaa....这种字符串的话复杂度就会高达O(n*m);

  2.哈希优化法:

    预处理出模式串和文本串的哈希,枚举一个起点用哈希O(1)判断是否相等,时间复杂度O(文本串);

    好像挺快的啊,那还学什么KMP啊,好了本文到此结束。

  既然哈希优化暴力枚举即可达到O(文本串)的优秀复杂度我,为何还要学习复杂度为O(文本串+模式串)的KMP算法呢?这与KMP的实现原理有关


KMP算法

  KMP思想

  仔细观察暴力枚举的漏洞,如果一个模式串在当前位置匹配完毕(无论失败成功),模式串首部只能前进一位,而中间可能包含大量无用匹配,这些匹配是之前已经确认失败的。

  而KMP思想的先进之处在于,每次模式串在当前位置匹配完毕之后,充分利用之前信息,将已经匹配成功的部分跳过从而减少时间复杂度

  举个栗子:

  文本串:abcabqwq

  模式串:abcabe

  在这两种模式串匹配过程中,显然模式串与文本串在匹配到第六位的时候会失配,在一般的算法中下一步会变成酱紫

  文本串:abcabqwq

  模式串:  abcabe

  然而我们观察到,如果文本串bcab...能够和模式串abcabe匹配上,那么模式串的e之前的四位字符应该与开头的四位字符相同才对,毕竟e之前的四位已经匹配成功了,但显然是不同的。

  继而我们得出一个结论:如果一个字符串移动后能够匹配上,那么已经匹配成功的子串的后几位字符应该与开头的几位字符相同。

  在模式串abcabe中,匹配成功的子串abcabc中只有ab与开头部分相同,那么我们大可以直接将ab移动到相应的位置上去:

  文本串:abcabqwq

  模式串:      abcabe

  如果移动以后仍然不能匹配成功呢?那么只要重复这个过程,直到下一位可以匹配上,或者模式串已经不能通过这种方式移动了,再向右继续匹配。

  为了不遗漏匹配,我们必须保证所有可能匹配成功的地方都扫过一遍,也就是说,我们模式串向右移动的距离应该尽可能短,这是为了保证正确性。

  所以我们要找模式串中每个前缀的最大后缀等于前缀(注意不能相等哦!),qwq有点拗口希望仔细理解一下,然后开一个数组(我们称为失配数组)记录这个后缀的长度,方便每次跳串(跳串什么鬼啦

  下文中我们称这个最大后缀等于前缀中的后缀为失配后缀,前缀为失配前缀,失配数组为nxt,文本串为s1,模式串为s2

  模式串前缀:a ab abc abca abcab abcabe

  失配数组:   0 0 0 1 2 0  

  失配数组获取:

    我们设置两个指针,i 负责扫描整个模式串,t 负责指向当前失配前缀。

    初始时刻 t 应滞后 i 一个位置,毕竟不能相等嘛qwq

    然后我们将要进行的是一个模式串自己匹配自己的过程,将 i 与 t 向右推进,每次失配时t2直接利用已经处理出来的失配数组向回跳

  栗子:

    模式串:a b a c a b

       t   i                               初始状态,nxt [ 1 ] = 0; 然而 s2 [ i+1 ]与s2 [ t+1 ]不匹配,且t无法向前跳,所以t不移动,++i;

    模式串:a b a c a b

         t       i                            此时nxt [ 2 ] = 0;,发现 s2 [ i+1 ]与s2 [ t+1 ] 匹配,++t;,++i;

    模式串:a b a c a b               

           t     i        nxt [ 3 ] =1,下一步s2 [ i+1 ]与s2 [ t+1 ]不匹配,t沿着nxt [ t ]数组向回跳,直到下一位匹配或者无法继续跳。

    实现方法有两种,总的来说第二种性质更优秀,但第一种更容易理解(个人认为),在这里两种方法都展示出来

    

inline void get_nxt()//第一种 
{
    int t1=0,t2;
    nxt[0]=t2=-1;
    while(t1<len2)
    {
        if(t2==-1||s2[t1]==s2[t2])
            nxt[++t1]=++t2;
        else    
            t2=nxt[t2];
    }
}

inline void get_nxt()//第二种 
{
    t=0;
    for(int i=1;i<len2;++i)
    {
        while(t&&s2[t]!=s2[i]) t=nxt[t];
        t+=(s2[t]==s2[i]);
        nxt[i+1]=t;
    }
}

inline void get_nxt()//第二种下标从1开始的写法 
{
    t=0;
    for(int i=2;i<=len2;++i)
    {
        while(t&&s2[t+1]!=s2[i]) t=nxt[t];
        t+=(s2[t+1]==s2[i]);
        nxt[i]=t;
    }
}

 

 

  匹配时更加简单,匹配成功就前进一位,失败就跳失配数组

 

inline void kmp()//第一种 对应 
{
    int t1=0,t2=0;
    while(t1<len1)
    {
        if(t2==-1||s1[t1]==s2[t2])
            t1++,t2++;
        else    t2=nxt[t2];    
        if(t2==len2)
        {
            //匹配成功,其他操作 
            t2=nxt[t2];
        }
    }
}
inline void kmp()//第二种 对应 
{
    t=0;
    for(int i=0;i<len1;++i)
    {
        while(t&&s2[t+1]!=s1[i]) t=nxt[t];
        t+=(s2[t+1]==s1[i]);
        if(t==len2-1)
        {
            //匹配成功,其他操作 
        }
    }
}
inline void kmp()//第二种 下标 1 开始 
{
    t=0;
    for(int i=1;i<=len1;++i)
    {
        while(t&&s2[t+1]!=s1[i]) t=nxt[t];
        t+=(s2[t+1]==s1[i]);
        if(t==len2)
        {
            //匹配成功,原地爆炸 
            t=nxt[t];
        }
    }
}

  看到这里相信各位有所感触:KMP算法精华在于nxt数组,它在O(文本串+模式串)的时间内还求出了模式串每个前缀的 最大后缀等于前缀 这一重要信息,相比之下,匹配文本倒是其次(毕竟单纯的匹配的话哈希爆搞也能搞过),该算法一些优秀的扩展大部分也是基于nxt数组的性质来实现的。


KMP应用

  1.最小后缀等于前缀

  洛谷P3435

  给定长度为n的字符串,求字符串每个前缀的最小后缀等于前缀。

  分析:这里设计到一个性质:失配前缀的后缀仍是失配后缀的一部分(毕竟失配前缀完全等同于失配后缀嘛)

  由于这条性质,我们只要一直沿着nxt数组向前跳,直到nxt [ t ] = 0,考虑到复杂度问题,我们在路径上加一个类似并查集的路径压缩操作;

  2.最短循环节

  POJ2406

  求一个字符串最多由几个循环节拼成

  分析:还记得我们的神仙结论吗?判断一个字符串[ l , r ]是否存在一个长度为d的循环节,只需判断 [ l+d , r ] 和 [ l , r-d ] 是否相等。 

  再联想nxt数组的内容:最大后缀等于前缀的长度。

  答案就呼之欲出了!

  最小循环节长度 res = len - nxt [ len ] ;

  最多循环节数量 ans = len / res ;

  这个神仙结论证明有点子麻烦我不想在这里写emmmmm,大家脑补一下好了。

  3.循环节的判断与修改

  HDU3746

  最少添加多少字符能获得循环节

  分析:首先若补充或补充最少,找到最小循环节一定是最优的,这里利用上面的最小循环节姿势。

  分类讨论: 若nxt [ len ] 等于0,则需要补充 len 个字符

  若nxt [ len ]不为0  若存在循环节,即len%(len - nxt [ len ])== 0 ,不需要补充字符

  不存在循环节,那么至少用补充(len - nxt [ len ])- (len % (len - nxt [ len ]))(最小循环节长度减去已经存在而不足的长度)

 

  51nod1554

  给定字符串,求有多少个前缀满足A+B+A+B+……+A,由k+1个A和k个B构成,其中A和B可以为空串。

  分析:

  我们可以将某一前缀串(后面称为当前串)分为两种类型:

  1.SSSSSSS类型:(S为最短循环节)

    我们可以求得S的个数为 num = len / (len - nxt [ len ]);

    那么既然要求以A结尾,最后的烂摊子必然是A处理,所以A就应该等于 num % k个S;

    那么完整的A+B个数就是 num / k 个;

    如果当前串满足A+B+A+B+……+A;那么A应该至少可以在A+B中占有一席之地,也就是说A的长度要小于等于A+B,剩下的作为B即可(B可以是空串,所以包括等于);

    即 num / k >= num % k;

  2.SSSSSSST类型:

    A继续收尾,所以A必然等于T , B只能等于 (S - T) + 数个S

    完整A+B个数仍然是num / k,T的个数是 num % k;

    由于这次 T ! = S , 所以成立条件变为 num / k > num % k;

  4.KMP优化DP

   CF1163D

  给定一个字符串c(0<=|c|<=10000<=c<=1000),c∗和小写字母组成,你可以给∗填上任何一个小写字母

  再给定两个字符串s,t,(0<=|s|,|t|<=50)(0<=s,t<=50),求如何填c中的*∗,使得c中包含s的次数 - c中包含t的次数最大

  分析:先咕咕咕,不太理解。(失配树锅已补,这个再咕咕两天)

  5.失配树(我自己起的名字qwq)

  洛谷P3426

  给定一个字符串,求可重叠的最小循环节。

  分析:(该题目分析大多摘自题解,加上了自己的一些理解,解释起来更通俗(至少我是这么认为的)

  可重叠最小循环节必定具有如下性质:

  1.是原字符串的前后缀

  2.假设有两个前缀 s 和 t 如果s<t(长度)且t是一个可重叠循环节,那么s也是一个可重叠循环节

  3.两个可重叠循环节起点之间距离不超过循环节长度

  对于性质1:只要跑一遍KMP,然后从nxt [ len ]一直往前跳,把路上的长度全部扔进栈,候选集合就完成了;

  对于性质2:从小到大枚举判断答案;

  对于性质3:如果从小到大枚举判断,把比当前循环节小的循环节中的节点全部删去,如果最大空隙小于当前循环节长度,那么该循环节就是答案;

     如何实现在枚举到一个循环节的时候删去所有比他小的循环节中的节点?

  我们可以考虑建一颗fail树(失配树),建立方式如下:

  

for(int i=1;i<=len;++i) add(nxt[i],i);

 

  这时每个节点既代表循环节长度,又代表了在原串中的位置

  由于性质1,所有答案一定在0到n这一条路径上;

  对于每个节点 x 满足以它为前缀和后缀的节点一定是它的子树;

  我们只要利用一个双向链表,每次删去非 x 子树的节点,然后更新最大空隙。

  当最大空隙小于当前节点(循环节长度时),该节点的值就是答案

 

   

#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read()
{
    int x=0,f=1;
    char ch;
    for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
    if(ch=='-') f=0,ch=getchar();
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
    return f?x:-x;
}
string s;
int len,res;
int nxt[500010];
int st[500010],top;
int pre[500010],to[500010];
struct point
{
    int nxt,to;
}a[1000010];
int head[500010],cnt;
inline void add(int x,int y)
{
    a[++cnt].nxt=head[x];
    a[cnt].to=y;
    head[x]=cnt;
}
inline void get_nxt()
{
    int t=0;
    for(int i=1;i<len;++i)
    {
        while(t&&s[t]!=s[i]) t=nxt[t];
        t+=(s[t]==s[i]);
        nxt[i+1]=t;
    }
    
    for(int i=1;i<=len;++i) add(nxt[i],i);
}
inline void del(int x)
{
    to[pre[x]]=to[x];
    pre[to[x]]=pre[x];
    res=max(res,to[x]-pre[x]);
    to[x]=pre[x]=0;
}
inline void bfs(int x,int y)
{
    queue<int> q;
    q.push(x);
    while(!q.empty())
    {
        int now=q.front();
        q.pop();
        if(now==y) continue;
        del(now);
        for(int i=head[now];i;i=a[i].nxt)
        {
            int t=a[i].to;
            q.push(t);
        }
    }
}
signed main()
{
    cin>>s;
    len=s.length();
    get_nxt();
    for(int i=len;i;i=nxt[i]) st[++top]=i,st[top+1]=0;
    for(int i=1;i<=len;++i) pre[i]=i-1,to[i]=i+1;
    res=1;
    for(int i=top;i;--i)
    {
        bfs(st[i+1],st[i]);
        if(res<=st[i])
        {
            printf("%lld\n",st[i]);
            break;
        }
    }
return 0;
}

 

posted @ 2019-07-24 21:00  lovelyred  阅读(340)  评论(0编辑  收藏  举报