KMP算法详解

16 篇文章 6 订阅
12 篇文章 12 订阅

一、引言

KMP算法(The Knuth-Morris-Pratt Algorithm)绝对是我到目前为止学过的最毒瘤的算法之一。我看KMP的代码就跟看天书一样,云山雾罩的。
在这里插入图片描述
这个KMP算法非常的玄妙,理解起来颇为费劲。本篇文章我会尽力讲的通俗易懂,用启发式的方法阐述Knuth、Morris和Pratt三位大神的思想。
(Knuth爷爷敲腻害的,Latex的发明人就是他~)

一些约定
匹配串text/long string,长的,也叫文本、主串(以下就以“长串”称呼),长度为 n n n数组记作l[0...n-1](l=long)
模式串pattern,短的,长度为 m m m数组记作p[0...m-1](p=pattern)
我们要在匹配串长的)里面找模式串短的)第一次出现的位置。

二、暴力匹配算法(Brute Force)

要提字符串匹配,肯定绕不开暴力匹配算法。优化算法就是建立在暴力的基础之上的。暴力算法很直观,就是枚举长串中的每个子串,判断这个子串是否与模式串相等
下面这张图展示了长串= ababcd \text{ababcd} ababcd模式串= abc \text{abc} abc的情况。
在这里插入图片描述
我们可以把算法流程形象地理解为:模式串从第 0 0 0位开始,每次向后移一位,然后比较上下两个字符串的对齐部分是否相等。如果相等,返回此时模式串的位置;如果都不想等,返回 − 1 -1 1

我们可以很轻松愉悦地写出这部分的代码:

int find(const char* l/*长的*/, const char* p/*短的*/)
{
	int n = strlen(l), m = strlen(p);
	// 分别为长串和模式串的长度 
	for(int i = 0; i <= n - m; ++i)
	// 枚举模式串在长串中出现的位置(相当于一直让模式串往后移)
	{
		bool flag = true; // 是否匹配
		for(int j = i; j < i + m; ++j)
		// 考察每个字符
		{
			if(l[j] != p[j - i]) // 如果对应字符不匹配
			{
				flag = false;
				break; // 失败,退出 
			} 
		}
		if(flag) // 若匹配成功
		{
			return i; // 返回匹配到的位置 
		} 
	}
	return -1; // 没有匹配的位置,返回-1
	// 如果有的话在前面就返回了,执行不到这里
}

因为思路很简单,这里就不过多赘述了。暴力匹配算法的时间复杂度为 O ( m ( n − m + 1 ) ) = O ( n m ) O(m(n-m+1))=O(nm) O(m(nm+1))=O(nm)

顺便说一下:STL中的string::find<cstring>中的函数strstr都采用的是暴力匹配算法。

三、好像可以优化一下?

当然,暴力算法在我们眼中看起来很蠢。有的时候明明很显然不可能匹配上的位置,暴力算法还要考察一下。有没有更聪明的办法呢?

下面为了更加直观,我们用色块表示字符串。相同颜色的色块表示相同的字符串。我们考虑一种非常特殊的情况。假如长串模式串的情况是这样的:
在这里插入图片描述
其中浅蓝色的部分是相同的字符串,两串中的“绿”部分是匹配的,且淡绿色的部分不包含浅蓝色的部分。而后面的红色黄色部分是不能匹配的。那我们下来怎么办呢?还傻乎乎地把模式串往后仅仅移一位吗?

显然,因为淡绿色部分不包含浅蓝色部分,仅仅往后移一位仍然是不可能匹配的。事实上,我们可以看出,下一个可能匹配的位置是这里:
在这里插入图片描述
只有让模式串跳到这个位置才可能进行下一个匹配,在此之前都是不可能匹配的。采用这种策略,我们可以直接跳很远,大幅度减少计算的次数

四、next数组

好了,推广到更一般的情况,现在我们发现长串模式串一部分是匹配的,且这部分是模式串的一个前缀。
在这里插入图片描述
那么,我们怎么知道绿色部分是否有前缀后缀是相等的呢?
在这里插入图片描述
要解决这个问题,就需要知道模式串的每个从头开始的子串最长的相等前缀后缀
在这里插入图片描述

几点解释:
(1) 从头开始的子串就是模式串的前缀,后面所谓的前缀可以理解为模式串的“前缀前缀”。
(2) 为什么要考虑每个从头开始的子串?因为你不知道长串模式串有多少是匹配的,所以得应对匹配部分长度等于任何值的情况。
(3) 为什么是最长的相等前缀后缀?因为要保证淡绿色的部分不可能包含浅蓝色的部分,这样我们才不会漏掉可能匹配的位置。举个例子,已经匹配的字符串是
在这里插入图片描述
它的最长的相等前缀后缀 aba \text{\color{#b4c6e7}aba} aba。那如果我们仅将 a \text{\color{brown}a} a作为浅蓝色部分的话,就必须这样跳:
在这里插入图片描述
但是,这忽略了一个很显然的情况:
在这里插入图片描述
在这个位置也是可能匹配的。
所以,为了避免“跳过了”,必须让前缀后缀的长度尽可能大。

那么,现在我们的任务就是求出每一个位置相等的前缀后缀的长度。
我们定义next数组,next[j]表示p[0...j-1]不等于自身的相等前缀后缀的长度(p为模式串)。(一定注意:是0~j-1,不包含第j个字符。这样定义可以简化代码。)即:令k = next[j],则p[0...k-1] = p[j-k+1...j]
在这里插入图片描述
p[0...j-1]没有相等的前缀后缀,记next[j] = 0。特别地,令next[0] = -1(后面讲为什么)。

举个栗子:
在这里插入图片描述
在字符串 abacaba \text{abacaba} abacaba中,next[6] = 2,因为p[0...5] = abacab,其最长相等前缀后缀 ab \text{ab} ab,长度为 2 2 2

小练习:求这个字符串的next数组: ijkjioijkji \text{ijkjioijkji} ijkjioijkji
答案: − 1 , 0 , 0 , 0 , 0 , 1 , 0 , 1 , 2 , 3 , 4 -1,0,0,0,0,1,0,1,2,3,4 1,0,0,0,0,1,0,1,2,3,4

现在考虑怎么求next数组。如果用暴力的话,时间复杂度高达 O ( m 3 ) O(m^3) O(m3)。然而,我们其实有 O ( m ) O(m) O(m)的算法。

递推是降低复杂度的一个有效方法。考虑已经知道了next[0...j],如何求出next[j+1]
回顾一下next的性质:设k = next[j],则p[0...k-1] = p[j-k...j-1]
现在对于next[j+1],如果说p[k] == p[j],那我们只需要把最长相等前缀后缀延长一格就好了,next[j+1] = next[j] + 1。如下图所示。
在这里插入图片描述
但如果p[k] != p[j],这就麻烦了。我们不能仅仅延长上一个位置的前缀后缀。怎么办呢?
假设最终我们求得的next[j+1] = t+1(加的 1 1 1是新增的相等的字符p[t] == p[j]),则p[0...t-1] = p[j-t...j-1],且t < k。而我们又有p[0...k-1] = p[j-k...j-1],那么把右边的p[j-k...j-1]映射到左边的p[0...k-1],就会得到p[k-t...k-1] = p[j-k...j-1]。结合p[0...t-1] = p[j-t...j-1]就有p[0...t-1] = p[k-t...k-1]。如下图所示:
在这里插入图片描述
这说明了什么?这说明了p[0...k-1]本身也有前缀后缀是相等的。因为next[j+1] = t+1t+1p[0...j]的最长相等前缀后缀的长度,那么t就是p[0...j-1]最长相等前缀后缀的长度。这里的最长保证了t同样也是p[0...k-1]最长相等前缀后缀的长度。因此:t = next[k]

这给我们一个思路:求next[j+1]的时候,令k = next[j],如果出现p[k] != p[j]的情况,就令k = next[k](此时k的值就是t),看看现在满不满足p[k] == p[j],如果满足,就令next[j+1] = k+1,否则就不断套娃,一直套到p[k] == p[j]为止。这样,让k从大往小依次尝试,只要尝试成功就给next[j+1]赋值并退出迭代,从而保证了k一定是尽可能大的。

如果一直不满足怎么办?在这种情况下必然会导致最终k == 0,如果此时仍有p[k] != p[j],再令k = next[k],就有k = -1。此时我们加个特判,如果k == -1,说明没有相等的前缀后缀,令next[j+1] = 0即可。

温馨提示next<algorithm>头文件中的一个函数名。如果你引用了该头文件,请避免使用next作为数组名。

这部分的代码实现

void GetNext(const char* p/*模式串*/, int* next/*模式串的next数组*/)
{
    int m = strlen(p); // m是模式串p的长度
    next[0] = -1; // 递推的初始条件,不要忘记
    for(int j = 0; j < m - 1; ++j) // 循环j从0到m-2
    {
        // 求next[j + 1]
        int k = next[j];
        while(k != -1)
        // 当k != -1时,一直套娃
        {
            if(p[k] == p[j])
            // 匹配成功,延长前缀和后缀
            {
                next[j + 1] = k + 1;
                break;
            }
            else k = next[k];
            // 否则继续套娃,寻找更短的相等前缀和后缀
        }
        if(k == -1) next[j + 1] = 0;
        // 匹配不成功,next[j + 1] = 0
    }
}

这份代码虽然没有板子代码简洁,但更容易理解。后面我们会讲解简洁的板子代码到底是如何运作的。

五、进行匹配

我们终于花了很大力气求出了next数组,现在就要派上用场了。匹配的部分比较简单,只要一直跳跳跳就行了。

回顾我们第三部分讲述的内容。当出现下面这种情况时:
在这里插入图片描述

就要让模式串跳到这里:
在这里插入图片描述
此时如果匹配,就万事大吉了;如果不匹配怎么办呢?
在这种情况下,我们看到,长串模式串仍然有浅蓝色部分是匹配的。其实我们可以继续套用刚才的过程。在浅蓝色的部分继续寻找有没有相等的前缀和后缀,如果有的话,那就可以继续跳:
在这里插入图片描述
其中黑框标出来的部分就是浅蓝色的部分的相等的前缀和后缀。如果还是不匹配,就对黑框中的部分继续套娃,一直套到匹配或者没有相等的前缀和后缀为止。

我们令一开始匹配的长度为j浅蓝色的部分长度为k = next[j]模式串的第j位与长串的第i位失配。
在这里插入图片描述
跳一次(模式串往右移j - k = j - next[j]格):
在这里插入图片描述
此时我们要判断l[i] == p[j']是否成立,其中j' = k = next[j]。所以我们可以直接把这个过程写成j = next[j]。这又成了类似于计算next数组一样的套娃过程。

当一直套到next[j] == 0长串模式串匹配部分没有相等的前缀和后缀,且l[i] != p[j]j = 0)时,再令j = next[j],就有j = -1,此时我们加一个特判:当j == -1时,就直接像暴力算法那样++ij = 0

注意:这里的i与暴力算法中的i含义不同。暴力算法中的i指的是l[i]p[0]对齐,而这里的il[i]p[j]对齐,与p[0]对齐的是l[i-j]。那么j = 0意味着l[i]p[0]对齐,就与暴力算法一样了。

这部分的代码实现:

int KMPSearch(const char* l/*长串*/, const char* p/*模式串*/)
{
    int n = strlen(l), m = strlen(p); // n和m分别是长串l和模式串p的长度
    int* next = new int[m]; // 开辟next数组的空间
    GetNext(p, next); // 调用GetNext函数获取模式串p的next数组
    int i = 0, j = 0; // l[i]与p[j]匹配
    while(i < n && j < m)
    {
        if(j == -1) // j == -1说明没有相等的前缀和后缀
        {
            ++i; // 匹配失败,i加一
            j = 0; // l[i]与p[0]对齐
        }
        if(l[i] == p[j]) // l[i]与p[j]匹配成功,继续往下
        {
            ++i;
            ++j;
        }
        else j = next[j];
        // 否则p往后跳,进行套娃
    }
    delete []next; // 释放next数组的空间
    if(j == m) return i - j; // 匹配成功,返回匹配的位置i-j
    else return -1; // 匹配失败,返回-1
}

六、举例说明

接下来我们举一个例子来理解(请配合代码食用)。
长串l abacabacabadabacabae \text{abacabacabadabacabae} abacabacabadabacabae
模式串p abacabae \text{abacabae} abacabae

🍒​next数组

m = strlen(p) = 8
next[0] = -1
然后j0循环到m-2

  • j=0k = next[j] = -1,匹配直接失败,next[j+1] = next[1] = 0
  • j=1k = next[j] = 0p[k] != p[j]'a' != 'b'),故令k = next[k]k = -1,匹配失败,next[j+1] = next[2] = 0
  • j=2k = next[j] = 0p[k] == p[j] == 'a',匹配成功,next[j+1] = next[3] = next[j]+1 = 0+1 = 1
  • j=3k = next[j] = 1p[k] != p[j]'a' != 'c'),令k = next[k] = next[2] = 0,而p[0] != p[j]'a' != 'c'),再次匹配失败,k = next[k] = -1next[j+1] = next[4] = 0
  • j=4k = next[4] = 0p[0] == p[j] == 'a',匹配成功,next[j+1] = next[j]+1 = 1
  • j=5k = next[5] = 1p[1] == p[j] == 'b',匹配成功,next[j+1] = next[j]+1 = 2
  • j=6k = next[6] = 2p[2] == p[j] == 'a',匹配成功,next[j+1] = next[j]+1 = 3

🥕进行匹配

n = strlen(l) = 20
next[]: {-1, 0, 0, 1, 0, 1, 2, 3}
l[]: "abacabacabadabacabae"
p[]: "abacabae"

首先,i = 0, j = 0,从头开始匹配。
进入while循环:
l[i] == p[j] == 'a'i++, j++。现在i = 1, j = 1
l[i] == p[j] == 'b'i++, j++。现在i = 2, j = 2
l[i] == p[j] == 'c'i++, j++。现在i = 3, j = 3

i = 6, j = 6时:
l[i] == p[j] == 'b'i++, j++。现在i = 7, j = 7
但是,l[7] != p[7]'c' != 'e'),进入elsej = next[j]j = 7next[j] = 3。现在j = 3(相等于p往后移4格)。
在这里插入图片描述
下面继续匹配到i = 10, j = 6。现在i = 11, j = 7,但i[11] = 'd'j[7] = 'e',不匹配,故令j = next[j],现在j = 3(相等于p往后移4格)。
在这里插入图片描述
但仍旧l[i] != p[j]'d' != 'c'),继续令j = next[j],现在j = next[3] = 1(相等于p往后移2格)。
在这里插入图片描述
仍然不匹配('d' != 'b'),令j = next[j] = 0(相等于p往后移1格):
在这里插入图片描述
仍然不匹配('d' != 'a'),令j = next[j] = -1,触发第一个if,所以i++, j = 0,从头再来:
在这里插入图片描述
下面一直可以匹配,直到j == m(注意最后多加了一个1,所以是m而不是m - 1),退出whie循环,返回pl中出现的位置i - j = 12
在这里插入图片描述

七、对简洁板子代码的解读

我们打比赛或者考试的时候肯定写的是很简单的代码。网上广为流传的简洁的代码如下:

int next[MAXN];// 这里将next设为全局变量,也可以在KMPSearch中动态分配内存

void GetNext(const char* p)
{
    int m = strlen(p), k = -1, j = 0; // p的长度,k和j(k=-1是因为next[0]=-1)
    next[0] = -1;
    while(j < m - 1) // j循环从0到m-2
        if(k == -1 || p[k] == p[j]) // 这里合并了两种情况
            next[++j] = ++k; // 更新next[j+1],且j、k加一
        else k = next[k]; // 套娃
}

int KMPSearch(const char* l, const char* p)
{
    int n = strlen(l), m = strlen(p), i = 0, j = 0; // 设一些变量
    GetNext(p); // 调用GetNext
    while(i < n && j < m)
        if(j == -1 || l[i] == p[j]) // 这里合并了两种情况
            ++i, ++j; // 往下匹配
        else j = next[j]; // 套娃
    if(j == m) return i - j; // 匹配成功
    else return -1; // 匹配失败
}

解读:
⭐️先看GetNext。与我的代码相比的区别主要是,合并了k == -1p[k] == p[j]两种情况。当k == -1时,应该让next[j+1] = 0,这里k++刚好让k = 0。这也是为什么我们定义next[0] = -1。当p[k] == p[j],需要next[j+1] = k+1,然后k++。这样做略去了k = next[j]的过程,也可以在合适的时候直接j++,不需要外层循环控制j
⭐️再看KMPSearch。同样合并了j == -1l[i] == p[j]两种情况。其中j == -1++j使得j = 0;其他的与我的代码差不多。

八、获取模式串所有出现的位置

两种方法:
(一)每次匹配成功后在下一位开始。这种方法效率稍低,注意只调用一次GetNext即可。
(二)每次匹配成功(j == m)后,令j = next[j],不break。此时我们需要知道next[m],因此GetNext中循环条件改为j <= m
这里我们给出洛谷P3375 【模板】KMP字符串匹配的代码:

#include <iostream>
#include <cstring>

using std::cin;
using std::cout;
using std::endl;
using std::strlen;
// 因为std::next与next数组冲突,所以不using namespace std

const int MAXN = 1e6 + 5;

char l[MAXN], p[MAXN];

int next[MAXN];

void GetNext(const char* p)
{
    int m = strlen(p), k = -1, j = 0;
    next[0] = -1;
    while(j < m) // 此处改上限
        if(k == -1 || p[k] == p[j])
            next[++j] = ++k;
        else k = next[k];
}

void FindAll(const char* l, const char* p)
{
    int n = strlen(l), m = strlen(p), i = 0, j = 0;
    GetNext(p);
    while(i < n)
    {
        if(j == -1 || l[i] == p[j])
        {
            ++i, ++j;
            if(j == m)
            {
                cout << i - j + 1 << '\n';
                // 题目要求下标从1开始
                j = next[j];
            }
        }
        else j = next[j];
    }
}

int main()
{
    cin.getline(l, MAXN);
    cin.getline(p, MAXN);
    int n = strlen(l), m = strlen(p);
    if(l[n - 1] == '\r') l[n - 1] = '\0', --n;
    if(p[m - 1] == '\r') p[m - 1] = '\0', --m;
    // 数据有奇怪的\r,需要去掉
    FindAll(l, p);
    for(int i = 1; i <= m; ++i) cout << next[i] << ' ';
    cout << endl;
    return 0;
}

九、最终BOSS:时间复杂度的证明

KMP算法的时间复杂度是 O ( n + m ) O(n+m) O(n+m)
⭐️GetNext函数:我的代码中有两层循环,貌似复杂度高于 O ( m ) O(m) O(m)。但是加起来其实还是 O ( m ) O(m) O(m)的,这与欧拉筛法有异曲同工之妙。

  • 我们看对简洁板子代码进行分析。我们发现,k只有在j增加的时候才会增加,j一定会增加m次,故k也增加m次。而每次k = next[k]的套娃过程都至少会让k减少 1 1 1,减少的次数显然不会超过增加的次数,所以减少的次数不会超过m次。因此while循环执行了不超过2m次,即GetNext的复杂度为O(m)
  • 对我的代码分析也可以得出这个结果。对于一个 j j j,设k = next[k]的执行次数为 f ( j ) f(j) f(j)次(每次k至少减少 1 1 1),则 f ( j ) ≤ next [ j ] f(j)\le\text{next}[j] f(j)next[j]。一开始k = next[j],进行了 f ( j ) f(j) f(j)次套娃后k至多为 next [ j ] − f ( j ) \text{next}[j]-f(j) next[j]f(j),所以 f ( j + 1 ) ≤ next [ j + 1 ] ≤ next [ j ] − f ( j ) + 1 f(j+1)\le\text{next}[j+1]\le\text{next}[j]-f(j)+1 f(j+1)next[j+1]next[j]f(j)+1
    所以 f ( m − 1 ) ≤ next [ m − 2 ] − f ( m − 2 ) + 1 ≤ next [ m − 3 ] − f ( m − 3 ) − f ( m − 2 ) + 2 ≤ next [ m − 4 ] − f ( m − 4 ) − f ( m − 3 ) − f ( m − 2 ) + 3 ≤ ⋯ ≤ next [ 0 ] − ∑ j = 0 m − 1 f ( j ) + m − 1 \begin{aligned}f(m-1)&\le\text{next}[m-2]-f(m-2)+1\\&\le\text{next}[m-3]-f(m-3)-f(m-2)+2\\&\le\text{next}[m-4]-f(m-4)-f(m-3)-f(m-2)+3\\&\le\cdots\\&\le\text{next}[0]-\sum\limits_{j=0}^{m-1}f(j)+m-1\end{aligned} f(m1)next[m2]f(m2)+1next[m3]f(m3)f(m2)+2next[m4]f(m4)f(m3)f(m2)+3next[0]j=0m1f(j)+m1移项得 ∑ j = 0 m − 1 f ( j ) ≤ next [ 0 ] + m − 1 − f ( m − 1 ) ≤ m − 2 \sum\limits_{j=0}^{m-1}f(j)\le\text{next}[0]+m-1-f(m-1)\le m-2 j=0m1f(j)next[0]+m1f(m1)m2所以 ∑ j = 0 m − 1 f ( j ) = O ( m ) \sum\limits_{j=0}^{m-1}f(j)=O(m) j=0m1f(j)=O(m),即套娃总共最多执行了 m − 2 m-2 m2次。

⭐️KMPSearch函数:KMP算法在不增加i的情况下不会增加j。因为i增加了n次,所以j至多减少n次,即j = next[j]的套娃不超过n次。因此这部分复杂度为 O ( n ) O(n) O(n)

综上,KMP的复杂度为 O ( n + m ) O(n+m) O(n+m)

答疑


🍎Q1在这里插入图片描述
在图示的情况下,若前缀后缀的长度为k = next[j](匹配部分为0~j-1),那么会不会出现前缀淡绿色交界处包含前缀的情况呢?(如下图所示)
在这里插入图片描述
在这种情况下,如果按我们介绍的往右跳j - next[j]格的方法,不就可能漏了前缀和方框部分对齐的情况吗?
在这里插入图片描述

A1:这确实是可能出现的,不过对结果没有影响。假设真的出现了这种情况,设前缀=PQP(P、Q是它的子串,方框与前缀公共部分是P),淡绿色部分=QPR(方框里面是QP,外面是R),黄色部分=S。
在这里插入图片描述
模式串=PQPQPRPQPS。如果真的在我们所说的地方匹配了的话,上图的红框部分是对应相等的,即RPQ=QPR。再看模式串0~j-1部分,取它的一个前缀PQP(QPR)P及后缀PQPRPQP,在前缀中代入QPR=RPQ得PQP(QPR)P=PQP(RPQ)P=后缀,故模式串0~j-1部分有比PQP还长的相等的前缀和后缀,与k = next[j]矛盾。所以如果真的能匹配,next值肯定很大,不会出现问题。如果不能匹配,跳过这里也没有任何关系。所以不需要担心这个问题。再次提醒:前缀后缀是可能重叠的~


例题解析

next数组的应用——洛谷P4391 [BOI2009]Radio Transmission 无线传输
题目大意:一个字符串 s 1 s_1 s1 s 2 s_2 s2不断拼接而成,现在给出 s 1 s_1 s1的一个子串,求 s 2 s_2 s2的最短长度。

例如,给出 s 1 = s_1= s1=cabcabca,则 s 2 = s_2= s2=cab,输出 3 3 3

分析:令 n = s t r l e n ( s 1 ) n={\rm strlen}(s_1) n=strlen(s1) m = s t r l e n ( s 2 ) m={\rm strlen}(s_2) m=strlen(s2)。考虑 s 1 s_1 s1的尾巴部分 s 3 s_3 s3 s 2 s_2 s2的一个子串,其长度为 p = ( n   m o d   m ) p=(n\ {\rm mod}\ m) p=(n mod m)。例子中 s 3 s_3 s3等于ca。又设 s 1 s_1 s1是由 N N N s 2 s_2 s2和一个 s 3 s_3 s3拼接而成(例子中 N = 2 N=2 N=2)。考虑 s 1 s_1 s1的长度为 n − m n-m nm的前缀 u u u,其由 N − 1 N-1 N1 s 2 s_2 s2和一个 s 3 s_3 s3拼接而成。再考虑 s 1 s_1 s1的长度为 n − m n-m nm的前缀 v v v,也由 N − 1 N-1 N1 s 2 s_2 s2和一个 s 3 s_3 s3拼接而成。所以 u = v u=v u=v n e x t [ n ] = n − m {\rm next}[n]=n-m next[n]=nm。所以答案是 n − n e x t [ n ] n-{\rm next}[n] nnext[n]
在这里插入图片描述
AC代码

#include <iostream>
#include <cstring>

using std::cin;
using std::cout;
using std::strlen;
using std::endl;

const int MAXN = 1e6 + 5;

char s[MAXN];
int next[MAXN];

int main()
{
    int n;
    cin >> n;
    cin.get(); // 略去'\n'
    cin.getline(s, MAXN);
    next[0] = -1;
    for(int i = 1; i < n; i++)
    {
        int k = next[i];
        while(k != -1)
            if(s[k] == s[i])
            {
                next[i + 1] = k + 1;
                break;
            }
            else k = next[k];
        if(k == -1) next[i + 1] = 0;
    }
    cout << n - next[n] << endl;
    return 0;
}

参考文献

  1. KMP Algorithm for Pattern Searching - Geeks for Geeks
  2. https://blog.csdn.net/v_JULY_v/article/details/7041827
  3. Knuth-Morris-Pratt (KMP) algorithm explained(有严谨的数学证明)
  4. What is the best & worst time complexity Of KMP algorithm
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值