字符串专题一:KMP与扩展KMP

KMP算法主要用于解决单模式串的匹配问题,即:给定主串s和模式串p,问p是否是s的子串(len(s)<=N, len(p)<=M)。

先考虑最朴素的算法,即枚举s中的起点i,检查s[i..i+M-1]是否等于p,这样的时间复杂度为O(NM)。

分析一下为什么这样的算法效率低(建议读者手动画个图):设指针i和j分别指向s和p中的字符,不妨假定s[0..k-1]和p[0..k-1]已经匹配上了,而s[k]!=p[k](k<M),这说明s[0..M-1]已经不能匹配上p了,根据朴素算法,指针i将移动到s[1],指针j将移动到p[0]重新开始匹配,之后每次失配时主串中的指针i都要回到前面,这就产生了不必要的麻烦。但是如果指针i不回溯而是停在s[k]处,就会漏掉s[l..l+M-1]和p[0..M-1](0<l<k)匹配上的情形。那么为了实现指针i不回溯,我们来重点考察这种情形:假定存在这样的l(0<l<k)使得s[l..l+M-1]和p[0..M-1]匹配上了,那么有s[l..k-1]=p[0..k-l-1],由于之前已经匹配上的部分说明了s[l..k-1]=p[l..k-1],于是推出p[0..k-l-1]=p[l..k-1],也就是说这样的l会使得p[0..k-1]的长度为k-l的前缀和后缀完全相同!如果令next[k]=k-l的话,那么一旦在p[k]处失配,指针i不需要回溯,而指针j只需要指向模式串的next[k]处继续与主串比较就可以了!这就是KMP算法思想的出发点。

那么接下来我们要解决的,就是如何计算next数组。设next[k]=r,根据上一段的分析我们知道r是使得p[0..k-1]的前缀和后缀完全相同的最大长度(之所以选择最大长度是因为,当再次失配的时候可以继续把指针j向前移,就会移到较小的使前后缀完全相同的长度上)。考虑利用递推的方式来求next数组,假定我们已知next[i]=j,现在求next[i+1]:

■■■■■■■■□___________■■■■■■■■□

0    j=next[i]           i

如果p[i]=p[j],那么自然有next[i+1]=j+1;如果p[i]!=p[j],那么从定义出发,next[i+1]是最大长度r(r<=j)使得p[0..r-1]=p[i-r+1..i],由next[i]的性质可以推出p[i-r+1..i]=p[i-r+1..i-1]+p[i]=p[j-r+1..j-1]+p[i],所以如果p[i]=p[r-1],那么r就满足p[0..r-2]=p[j-r+1..j-1],这表明next[j]=r-1,即next[i+1]=next[j]+1,此时相当于在比较p[i]和p[next[j]];如果仍然有p[i]!=p[r-1],那么仿照上面的推导过程,只需要比较p[i]和p[next[next[j]]],形成这样一个递归过程:不断令j=next[j],直到最后p[i]等于p[j]或者j=-1为止(如果令next[0]=-1的话),然后令next[++i]=++j就可以了。下面是对模式串p计算next数组的代码:

void CountNext(char p[])
{
    int i = 0, j = -1;
    next[0] = -1;
    while (i != lenp)
        if (j == -1 || p[i] == p[j])
            next[++i] = ++j;
        else j = next[j];
}

有了next数组以后就可以实现第一段中陈述的匹配过程了,用指针i指向主串s,指针j指向模式串p,如果s[i]=p[j]或者j=-1,那么两个指针各向后移动一位,如果s[i]!=p[j],那么就将j向前移动到next[j]处,即令j=next[j]。当j=len(p)的时候,匹配就成功了,而当i=len(s)的时候,匹配就失败了。下面是利用next数组进行单模式串匹配的代码:

void KMP(char s[], char p[])
{
    int i = 0, j = 0;
    while (i != lens)
    {
        if (j == -1 || s[i] == p[j])
            ++i, ++j;
        else j = next[j];

        if (j == lenp)
            printf("One matched!\n"), j = next[j];
    }
}

可以看出两段代码具有很高的相似度。上面这段代码中,将完成一次匹配看作是在p[len(p)]处失配,这样就可以继续查找主串后面的部分与模式串的匹配情况了。

整个KMP算法的时间复杂度为O(N+M),效率非常高。

练习题列表:

POJ3461 - Oulipo

Solutions

POJ3461 - Oulipo

题意:求模式串p在主串s中出现的次数(len(p)<=10,000, len(s)<=1,000,000)。

KMP模板题,每次匹配成功计数器自增1即可。

 1 //  Problem: poj3461 - Oulipo
 2 //  Category: KMP Algorithm
 3 //  Author: Niwatori
 4 //  Date: 2016/07/23
 5
 6 #include <stdio.h>
 7 #include <string.h>
 8
 9 int next[10010];
10 int lens, lenp;
11
12 void CountNext(char p[])
13 {
14     int i = 0, j = -1;
15     next[0] = -1;
16     while (i != lenp)
17         if (j == -1 || p[i] == p[j])
18             next[++i] = ++j;
19         else j = next[j];
20 }
21
22 int KMP(char s[], char p[])
23 {
24     int i = 0, j = 0, cnt = 0;
25     while (i != lens)
26     {
27         if (j == -1 || s[i] == p[j])
28             ++i, ++j;
29         else j = next[j];
30
31         if (j == lenp)
32             ++cnt, j = next[j];
33     }
34     return cnt;
35 }
36
37 int main()
38 {
39     int t; scanf("%d", &t);
40     while (t--)
41     {
42         char p[10010], s[1000010];
43         scanf("%s%s", p, s);
44         lenp = strlen(p);
45         lens = strlen(s);
46         CountNext(p);
47         printf("%d\n", KMP(s, p));
48     }
49     return 0;
50 }
51
52 void KMP(char s[], char p[], int n)
53 {
54     int i = 0, j = 0;
55     while (i != lens)
56     {
57         if (j == -1 || s[i] == p[j])
58             ++i, ++j;
59         else j = next[j];
60
61         if (j == lenp)
62             printf("One matched!\n"), j = next[j];
63     }
64 }

时间: 2024-10-07 21:51:35

字符串专题一:KMP与扩展KMP的相关文章

KMP与扩展KMP

原文转自:http://www.cppblog.com/MatoNo1/archive/2011/04/17/144390.aspx KMP:给出两个字符串A(称为模板串)和B(称为子串),长度分别为lenA和lenB,要求在线性时间内,对于每个A[i] (0<=i<lenA),求出A[i]往前和B的前缀匹配的最大匹配长度,记为ex[i](或者说,ex[i]为满足A[i- z+1..i]==B[0..z-1]的最大的z值).KMP的主要目的是求B是不是A的子串,以及若是,B在A中所有出现的位置

KMP和扩展KMP【转】

这种东西基本上在纸上自己推导一下就能做出来XD 转发注明出处 KMP:给出两个字符串A(称为模板串)和B(称为子串),长度分别为lenA和lenB,要求在线性时间内,对于每个A[i] (0<=i<lenA),求出A[i]往前和B的前缀匹配的最大匹配长度,记为ex[i](或者说,ex[i]为满足A[i- z+1..i]==B[0..z-1]的最大的z值).KMP的主要目的是求B是不是A的子串,以及若是,B在A中所有出现的位置(当 ex[i]=lenB时).[算法]设next[i]为满足B[i-z

KMP与扩展KMP初探

KMP KMP算法主要用于字符串匹配中的单串匹配 next函数:表示当前字符失配时,应从模式串的第几位开始匹配(越大越好).即模式串的前缀与以t[i]为结尾的后缀的最长相同部分的长度. 代码如下(pascal) var s,t:string; next,ans:array[0..100] of longint; i,j:longint; begin readln(s); readln(t); next[1]:=0; {构造next} j:=0; for i:=2 to length(t) do

KMP、扩展KMP、MANACHER

一.KMP 作用:用于在一个文本串S内查找一个模式串P出现的位置 string str = "bacbababadababacambabacaddababacasdsd"; string ptr = "ababaca"; 如上图,可得在第10与26处包含ptr数组: 暴力做法:暴力for,碰到不一样的直接返回,从后一个开始继续for,最差能到O(n * m) KMP 做法: 主要的思路是跳,比如你一开始从上面例子里的bacbababadababacambabacad

KMP、扩展KMP、Manacher模板

推导过程推荐看这篇: KMP模板: 1 void init(){ 2 int j = nex[0] = -1, i = 0; 3 int len = strlen(str); 4 while(i < len){ 5 if(j == -1 || str[i] == str[j]){ 6 nex[++i] = ++j; 7 }else j = nex[j]; 8 } 9 } 10 int KMP(){ 11 int i = 0, j = 0, sum = 0; 12 int len = strlen

POJ--1699--Best Sequence【扩展KMP+DFS】

链接:http://poj.org/problem?id=1699 题意:给出n个字符串,求他们相连的最小长度,如果首尾字母相同则可以共用相同部分,比如两个串ABCDEF和DEFGHI,他们相连为ABCDEFGHI,最小长度为9,中间的DEF部分共用了. 思路:由于数据量较小,首先对每两个字符串a,b用扩展KMP求出a连在b之后可以共用的长度,用数组B[i][j]表示第j个字符串连接在第i个字符串后面能共用的最大长度. 1.在扩展KMP函数中求子串a和母串b的公共前缀数组ret[i](子串.母串

HDU 4333 Revolving Digits 扩展KMP

链接:http://acm.hdu.edu.cn/showproblem.php?pid=4333 题意:给以数字字符串,移动最后若干位到最前边,统计得到的数字有多少比原来大,有多少和原来相同,有多少比原来的小. 思路:拓展KMP中的next数组标记的是子串和母串的公共前缀的长度,要将字符串长度变成原来二倍,这样如果变换后不是完全相同的数字也即公共前缀长度大于等于字符串长度,那么字母串公共前缀的下一位的大小比较就是题目所要求的比较.由于相同的数字串只算一次,则只要统计比较第一个"循环节"

扩展KMP复习小记

简介 KMP大家都耳熟能详,扩展KMP只是一个扩展版而已,字面意思啦! 我记得以前打过这个复习小记的,但是不知为何失踪了. KMP与扩展KMP的对比 KMP的next[i]表示从1到i的字符串s,前缀和后缀的最长重叠长度. EXKMP的next[i]表示从1到i的字符串s,和从i到n的字符串st的最长重叠长度. 也就是说KMP是向前的匹配,EXKMP是向后匹配. 扩展KMP问题是KMP问题的补充和加难. 具体内容 重要数组 给定母串S,和子串T.定义n=|S|, m=|T|. extend[i]

【扩展kmp+最小循环节】HDU 4333 Revolving Digits

http://acm.hdu.edu.cn/showproblem.php?pid=4333 [题意] 给定一个数字<=10^100000,每次将该数的第一位放到放到最后一位,求所有组成的不同的数比原数小的个数,相等的个数,大的个数 [思路] 这个数很大,用字符串处理 比较两个字符串的大小,一位一位很耗时,可以求出最长公共前缀,只比较最长公共前缀后一位 每次将数的最后一位放到最后一位,如abcd变成dabc,cdab,bcda,相当于abcdabcd各个后缀的前四位 这样就变成了求abcdabc