文章目录
一、引言
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(n−m+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+1
,t+1
是p[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
时,就直接像暴力算法那样++i
,j = 0
。
注意:这里的
i
与暴力算法中的i
含义不同。暴力算法中的i
指的是l[i]
与p[0]
对齐,而这里的i
是l[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
然后j
从0
循环到m-2
j=0
:k = next[j] = -1
,匹配直接失败,next[j+1] = next[1] = 0
j=1
:k = next[j] = 0
,p[k] != p[j]
('a' != 'b'
),故令k = next[k]
,k = -1
,匹配失败,next[j+1] = next[2] = 0
j=2
:k = next[j] = 0
,p[k] == p[j] == 'a'
,匹配成功,next[j+1] = next[3] = next[j]+1 = 0+1 = 1
。j=3
:k = next[j] = 1
,p[k] != p[j]
('a' != 'c'
),令k = next[k] = next[2] = 0
,而p[0] != p[j]
('a' != 'c'
),再次匹配失败,k = next[k] = -1
,next[j+1] = next[4] = 0
。j=4
:k = next[4] = 0
,p[0] == p[j] == 'a'
,匹配成功,next[j+1] = next[j]+1 = 1
。j=5
:k = next[5] = 1
,p[1] == p[j] == 'b'
,匹配成功,next[j+1] = next[j]+1 = 2
。j=6
:k = next[6] = 2
,p[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'
),进入else
,j = next[j]
。j = 7
,next[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
循环,返回p
在l
中出现的位置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 == -1
和p[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 == -1
和l[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(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−1f(j)+m−1移项得 ∑ 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=0∑m−1f(j)≤next[0]+m−1−f(m−1)≤m−2所以 ∑ j = 0 m − 1 f ( j ) = O ( m ) \sum\limits_{j=0}^{m-1}f(j)=O(m) j=0∑m−1f(j)=O(m),即套娃总共最多执行了 m − 2 m-2 m−2次。
⭐️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
n−m的前缀
u
u
u,其由
N
−
1
N-1
N−1个
s
2
s_2
s2和一个
s
3
s_3
s3拼接而成。再考虑
s
1
s_1
s1的长度为
n
−
m
n-m
n−m的前缀
v
v
v,也由
N
−
1
N-1
N−1个
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]=n−m。所以答案是
n
−
n
e
x
t
[
n
]
n-{\rm next}[n]
n−next[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;
}