• 【算法】KMP算法


    upd: 2021.4.13

    目录

    简介
    前置知识
    原理
    例题

    简介

    KMP 算法由 (Knuth-Morris-Pratt) 三位科学家提出,可用于在一个 文本串 中寻找某 模式串 存在的位置。
    本算法可以有效降低在一个 文本串 中寻找某 模式串 过程的时间复杂度。(如果采取朴素的想法则复杂度是 (O(MN))

    题面:https://www.luogu.com.cn/problem/P3375

    这里朴素的想法指的是枚举 文本串 的起点,然后让 模式串 从第一位开始一个个地检查是否配对,如果不配对则继续枚举起点。

    前置知识

    真前缀
    指字符串左部的任意子串(不包含自身),如 abcde 中的 a,ab,abc,abcd 都是真前缀但 abcde 不是。

    真后缀
    指字符串右部的任意子串(不包含自身),如 abcde 中的 e,de,cde,bcde 都是真后缀但 abcde 不是。

    前缀函数
    一个字符串中最长的、相等的真前缀与真后缀的长度, 如AABBAAA对应的前缀函数值是 (2)

    原理

    注意:在分析的时候,我们规定字符串的下标从 (1) 开始。

    开始:
    我们记扫描模式串的指针为j,而扫描文本串的指针为i,假设一开始i,j都在起点,然后让它们一直下去直到完全匹配或者失配,比如:

    j
    ABCD
    
    i
    ABCDEFG
    

    然后

     j
    ABCD
    
     i
    ABCDEFG
    

    最后在此完成了一次匹配,类似地如果ABCD改为ABCC则在此失配。

       j
    ABCD
    
       i
    ABCDEFG
    

    i,j运作模式如上。



    KMP算法就是,当模式串和文本串失配的时候,j指针从真后缀的末尾跳到真前缀的末尾,然后从真前缀后一位开始继续匹配。(从而起到减少配对次数,这便是KMP算法的核心原理)

    一个比较感性的理解是:尽可能地减少必然失败的匹配。

    结合例子解释:

    模式串: (AABBAAA)

    文本串: (AABBAABBAAA)

    j指针在最后一个A处失配。

          j
    AABBAAA
          i
    AABBAABBAAA
    

    因为此时 (j) 为尾的前缀 所对应的前缀函数值是 (2) ,所以 j指针 跳到这里:

     j
    AABBAAA
          i
    AABBAABBAAA
    

    然后从下一位开始继续配对:

      j
    AABBAAA
          i
    AABBAABBAAA
    

    最后

          j
    AABBAAA
              i
    AABBAABBAAA
    

    可以看出,KMP 能够有效减少配对次数。

    实现

    我们记模式串p文本串s

    从上面的模拟中,我们发现需要预处理出一个数组(记之为next[]),它储存模式串中前缀对应的前缀函数(pi()),如对于字符串ABCABC

    (pi(0)=0) (因为什么都没有)
    (pi(1)=0)A甚至没有真前缀真后缀
    (pi(2)=0)AB
    (pi(3)=0)ABC
    (pi(4)=1)ABCA
    (pi(5)=2)ABCAB
    (pi(6)=3)ABCABC

    同样地,我们发现如果用暴力朴素的想法来统计复杂度是 (O(N^2)) 不好,于是采用类似于上面的方法,只不过模式串配对的对象是自己罢了。

    可以结合代码理解,并注意举例,尝试在纸上模拟这个过程。

    for(int i=2,j=0;i<=lenp;i++){
            while(j && p[j+1]!=p[i]) j=nxt[j]; // 如果j指向元素的下一个元素会和当前配对位置失配,则j跳回去
            if(p[j+1]==p[i]) j++; //如果能够配对上,j++
            nxt[i]=j; //记录当前位置的前缀函数π
    }
    

    完整代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1e6+5;
    char p[N],s[N];
    int nxt[N];
    
    int main(){
        cin>>s+1>>p+1;
    
        int lenp=strlen(p+1),lens=strlen(s+1);
        // build next array
        for(int i=2,j=0;i<=lenp;i++){
            while(j && p[j+1]!=p[i]) j=nxt[j]; // 如果j指向元素的下一个元素会和当前配对位置失配,则j跳回去
            if(p[j+1]==p[i]) j++; //如果能够配对上,j++
            nxt[i]=j; //记录当前位置的前缀函数π
        }
    
        for(int i=1,j=0;i<=lens;i++){
            while(j && p[j+1]!=s[i]) j=nxt[j];
            if(p[j+1]==s[i]) j++;
    
            // if match
            if(j==lenp){
                j=nxt[j];
                cout<<i-lenp+1<<endl;
            }
        }
    
        for(int i=1;i<=lenp;i++) cout<<nxt[i]<<' ';
        cout<<endl;
    
        return 0;
    }
    

    复杂度

    (O(N+M))

    例题

    https://www.acwing.com/problem/content/1054/

    分析

    结合了状态机,KMP的一道DP题。
    递推方程:f[i][u]+=f[i-1][j] 其中j表示一个状态,u表示由j转移而来的状态,i表示填到文本串的第几位了。

    代码
    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=55, mod=1e9+7;
    
    int nxt[N],f[N][N];
    
    int n;
    char p[N];
    
    int main(){
        cin>>n>>p+1;
        
        int lenp=strlen(p+1);
        for(int j=0,i=2;i<=lenp;i++){
            while(j && p[j+1]!=p[i]) j=nxt[j];
            if(p[j+1]==p[i]) j++;
            nxt[i]=j;
        }
        
        f[0][0]=1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<lenp;j++)
                for(char k='a';k<='z';k++){
                    int u=j;
                    while(u && p[u+1]!=k) u=nxt[u];
                    if(p[u+1]==k) u++;
                    
                    if(u<lenp) f[i][u]=(f[i-1][j]+f[i][u])%mod;
                }
        
        int res=0;
        for(int i=0;i<lenp;i++) res=(f[n][i]+res)%mod;
        
        cout<<res<<endl;
        
        return 0;
    }
    

    https://www.luogu.com.cn/problem/CF1200E

    分析

    思路很简单的一道题,我们记已经拼接好的串为res,然后新输入的串为rec,只需建立一个tmp将它们以rec在前,res在后的顺序拼成一个串,用 KMP 算出这个串的前缀函数,从而得到rec需要将头部删去多少个数再拼上res即可,但直接这样做会导致后面res的长度非常大,从而导致 KMP 的时候TLE,所以要取recres较短的长度切片再拼接,细节见代码。

    代码
    #pragma GCC optimize("O3")
    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1e6+5;
    int nxt[N];
    string res,rec; // 答案串、记录输入的串
    
    int main(){
        ios::sync_with_stdio(false);
        int n;
        cin>>n;
    
        n--; // 先读入res中
        cin>>res;
        while(n--){
            cin>>rec;
    
            string tmp;
            int len=min(rec.size(),res.size());
    
            tmp=' '+rec.substr(0,len)+"!";
     //将串的下标定为 1 ,同时加一个其他符号防止前缀函数值比min(rec,res)还大
            tmp+=res.substr(res.size()-len,len);
    
            //kmp
            nxt[0]=nxt[1]=0;
            for(int i=2,j=0;i<tmp.size();i++){
                while(j && tmp[j+1]!=tmp[i]) j=nxt[j];
                if(tmp[j+1]==tmp[i]) j++;
                nxt[i]=j;
            }
    
            int x=nxt[tmp.size()-1];
            rec.erase(0,x);
            res+=rec;
     //因为这里我卡了很久,如果写成res=res+rec 会导致TLE 因为这样写的复杂度是O(N^2)而res+=rec是线性复杂度
        }
    
        cout<<res<<endl;
    
        return 0;
    }
    
  • 相关阅读:
    Javascript中的Math.max()和Math.min()
    附件预览项目采坑记
    移动设备后台的理解
    网络是怎么连接的?(进阶一)
    Git建立本地分支和远程分支的映射关系
    springboot使用redis实现发布与订阅
    centos下安装配置mongodb
    vue循环时设置多选框禁用状态,v-for
    谷歌浏览器postman插件安装,亲测可用
    element-ui 无法对绑定表单的对象中的对象属性进行验证
  • 原文地址:https://www.cnblogs.com/Tenshi/p/14400831.html
Copyright © 2020-2023  润新知