kmp从入门到放弃

kmp从入门到放弃

标签 : kmp、扩展kmp


KMP


给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。
A= “aaaaaaaaaaaaaaaaaaaaaaaaaab”,B=”aaaaaaaab” (最坏状态)

O (mn): 一般做法

    for (枚举B在A串中的起始位置)
        for(向后比较AB是否相等)
            ……

O(n+m):传说中的KMP算法。

之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。

  • 定义:
    S[1… n] 目标串 T[1…m] 模式串

  • 算法目的:

    从目标串S中找到一个子串和模式串T完全匹配。

  • 算法核心思想:

    1. 枚举i从1到n,在S[i-j…i-1]和T[1…j]完全匹配的前提下,判断S[i]是否和T[j+1]相等:
      (a) 如果相等,说明S[i-j…i]和T[1…j+1]完全匹配,那么i和j都自增1;
      (b) 如果不相等,则需要找到一个最大的k < j,满足S[i-k…i-1]和T[1…k]完全匹配;

    2. 当i=n或j=m的时候说明匹配结束,否则重复(1);

  • Next数组

    Next[j]表示在模式串T中以第j个元素为结尾的后缀中满足它是T的前缀最长的后缀的长度。
    i: 1 2 3 4 5 6
    s[i]: a b c d a b
    next[i]: 0 0 0 0 1 2

    i: 1 2 3 4 5 6 7 8 9 10 11 12 13
    s[i]: a b c a b d f g a b c a b
    next[i]: 0 0 0 1 2 0 0 0 1 2 3 4 5

    字符串下标从0开始,next数组下标从1开始
    此处输入图片的描述

    字符串下标从1开始,next数组下标从1开始

void getnext (int l,char *s){   // 字符串下标从0开始
     int i,j;
     next[1]=0;
     for (j=0,i=2;i<=l;i++){     // j为模式串的起始位置
        while (j>0&&s[i]!=s[j+1])
              j=next[j];      // 在str[i-j i-1]和str[1j] 完全匹配的前提下判断str[i]和str[j+1]是否相等
                               // 如果不相等,则减小j的值,直到匹配到完全相等位置
        if (s[i]==s[j+1])
            j++;
                                        // 如果能够找到以i结尾的后缀和以j+1结尾的前缀完全匹配,j自增1。
                                        // 这里j有两种情况:
                                        // j = 0    以i结尾的后缀找不到一个前缀和它完全匹配
                                        // j > 0    以i结尾的后缀和以j结尾的前缀完全匹配,更新Next函数的值
         next[i]=j;
    }
}

ps.给大家补几个图,希望有助于大家理解。
科赫曲线
(科赫曲线)


        int KMP(int n, char *S, int m, char *T) {
            int cnt=0;
            for(int j=0,i=1;i<=n;i++) {
                while(j>0&&S[i]!=T[j+1])j=next[j];
                if(S[i]==T[j+1])j++;
                if(j==m) {
                    cnt++;
                    j=next[j];
                }
            }
            return cnt;
        }
  1. PKU 3461 Oulipo

    题意:求一个匹配串T在目标串S中的出现次数。
    题解:求出T的Next数组,然后和S进行KMP匹配,匹配时当j=m的时候表示找到一个可行解,计数器+1,然后将Next[j]赋值给j,使得它的最长前缀能够继续和目标串进行匹配。
    
  2. 海哥的题

  3. (海哥2.0) HDU 2594 Simpsons’ Hidden Talents

    题意:给定两个长度不大于50000的串,求两个串的一个最长公共子串满足子串为第一个串的前缀,并且为第二个串的后缀。
    
    题解:将s2连在s1的后面,再对字符串求next数组
    
     #注意#  s1和s2相连时可能连接的部分出现额外的匹配
    如:(abd)     (cab qqqq abdcab)
        解决方法:
    1、用一个从未使用过的字符链接s1和s2  => abd  cab*qqqqabdcab
    2、在处理之后 
            u=next[len1+len2];
            while(u>min(len1,len2))u=next[u];
    
  4. PKU 2406 Power Strings
    题意:给定一个长度不超过N(N <= 106)的字符串,它一定是某个串重复K次得到,求这个K的最大值。

    题解:假设子串T重复K次后得到串S,那么T的长度一定为L = N/K(要整除),则T=S[1...L],将S拆分成K份,每份长度为L,则有
    S[1...L] = S[L+1...2L] = S[2L+1...3L] = ... = S[(K-1)L+1...KL]
    由于要保证K最大,势必L要取最小,所以根据Next函数的定义,有Next[KL] = (K-1)L;即Next[N] = N - L,所以L = N - Next[N];
    但是得出的长度L还要保证能被N整除,所以如果不能整除说明L = N,即K = 1;而如果能整除,那么K = N / (N - Next[N]);
    
  5. HDU 3374 String Problem

    题意:给定一个长度为N(N <= 106)的字符串S,然后将它进行左移,总共产生N个循环字符串,求其中字典序最小的串的编号以及这样的串的个数,和字典序最大的串的编号以及这样的串的个数。
    
    题解:KMP+最大最小表示法
    先求最小最大字典序
    

    定义两个指针i,j,i初始为0,j初始为1,再定义一个长度变量k = 0:
    1) 比较S[i+k] 和S[j+k]的大小关系:
    a) 如果相等,k自增1;当k==N则跳出循环,否则继续1)的比较;
    b) 如果S[i+k] < S[j+k],j += k + 1, k = 0;
    c) 如果S[i+k] > S[j+k], i += k + 1, k = 0;
    2) 如果i 和j相等,j自增1;当j==N或i==N则跳出循环,否则继续1)的比较;

    这样循环结束后如果,取i和j的小者就是答案。
    

    (1)然后在利用求出来的下标,生成一个新的字符串作为匹配串和一个原串的两倍的串作为目标串进行KMP匹配,得到种数。
    (2)直接用next数组求出循环节,再计算出周期即可。


#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;

int l,ne[2000007],pmax,pmin,ans;
char s[2000007];

int find_min_max(bool ifmin){
    int i,j,k;
    i=1;j=2;k=0;
    while (j<=l&&i<=l&&k<l){
        if (s[i+k]==s[j+k]){ // 注意这里可以自身扩展,或者用mod
            k++;
            if (k==l)break;
        }
        else if (s[i+k]<s[j+k]){
            if (ifmin)
                j+=k+1;
            else i+=k+1;
            k=0;
        }
        else {
            if (ifmin)
                i+=k+1;
            else j+=k+1;
            k=0;
        }
        if (i==j){
            j++;
        }
    }
    return min (i,j);
}

void getnext(){
    int i,j;
    ne[1]=0;
    for (j=0,i=2;i<=l;i++){
        while (j>0&&s[i]!=s[j+1])j=ne[j]; 
        if (s[i]==s[j+1])j++;
        ne[i]=j;
    }
}

void work (){
    int i,u;
    l=strlen (s+1);
    for (i=l+1;i<=l+l;i++)s[i]=s[i-l];// 注意这里的要自身扩展方便做最小字典序
    pmin=find_min_max(1);
    pmax=find_min_max(0);
    getnext ();
    u=l-ne[l];
    if (l%u)
        ans=1;
    else 
        ans=l/u;
    printf ("%d %d %d %d\n",pmin,ans,pmax,ans);
}

int main (){
    while (~scanf ("%s",s+1)){
        work ();
    }
    return 0;
}

6.PKU 3690 Constellations

    题意:给定N*M(N<=1000, M <= 1000)的01矩阵S,再给定T(T <= 100)个P*Q(P <= 50, Q <= 50)的01矩阵,问P*Q的矩阵中有多少个是S的子矩阵。

    题解:由于P <= 50,所以我们可以把所有P*Q的矩阵进行二进制位压缩,将P*Q的矩阵的每一列压缩成一个64位整数,这样P*Q的矩阵就变成了一个长度为Q的整数序列T,用同样的方式对N*M的矩阵进行压缩,总共可以产生(N-P+1)个长度为M的整数序列,剩下的就是进行最多(N-P+1)次KMP匹配了。

扩展KMP


扩展的KMP问题

给定母串S,和子串T。

定义n=|S|, m=|T|,extend[i]=S[i..n]与T的最长公共前缀长度。在线性的时间复杂度内,求出所有的extend[1..n]。

容易发现,如果有某个位置i满足extend[i]=m,那么T就肯定在S中出现过,并且进一步知道出现首位置是i——而这正是经典的KMP问题。
因此可见“扩展的KMP问题”是对经典KMP问题的一个扩充和加难。

aaaaabaa
aaaaaa
extend[1]=5 (6次运算)

aaaaabaa
 aaaaaa
extend[2]=4 (?次运算)


next[2]=5
S[1..5]=T[1..5]
=>
T[2..6]=T[1..5]
T[2..5]=T[1..4]
S[2..5]=T[1..4]

T[2]开始的比较是完全可以避免前4次比较,我们直接从S[6]→T[5]开始比较。这时候一比较就发现失配,因此extend[2]=4
  • 下面提出一般的算法。

设extend[1..k]已经算好,并且在以前的匹配过程中到达的最远位置是p。最远位置严格的说就是i+extend[i]-1的最大值,其中i=1,2,3,…,k;不妨设这个取最大值的i是a。

设next[i]为满足B[i..i+z-1]==B[0..z-1]的最大的z值(也就是B的自身匹配)。设目前next[0..lenB-1]与ex[0..i-1]均已求出,要用它们来求ex[i]的值。
设p为目前A串中匹配到的最远位置,k为让其匹配到最远位置的值(或者说,k是在0<=i0


void GetExtand(const EleType str[], int strLen, int extand[], const EleType mode[], int modeLen, int next[])
{
    int i, a, p, j(-1);

    for (i = 0; i < strLen; ++i, --j)
    {
        if (j < 0 || i + next[i - a] >= p)
        {
            if (j < 0) j = 0, p = i;
            while (p < strLen && j < modeLen && str[p] == mode[j])
                ++p, ++j;
            extand[i] = j, a = i;
        }
        else extand[i] = next[i - a];
    }
}

小结:
1、KMP算法充分利用已知信息
2、“字符串”

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值