2020.3.19编辑:
看到当年写的拙劣理解实在是不堪入目。。。虽然不是错漏百出,但也难免误人子弟…
如果有同学看到这里,请牢记理解kmp最关键的两点:
- 画图理解,使用可以区分正反顺序的字符串(而不是简单的一个字母)来代表相等的部分
- 关于正确性,结合border理论可以自然地反证。
KMP
字符串匹配。
给出一个主串s和另一个串f,问f是否s的子串。
暴力的话最坏复杂度要到O(mn),而用KMP的话通过一个O(m)的预处理就可以O(n)的 处理出结果。
因为只需要处理子串f,所以特别适合于给出一大堆s串,叫你求子串的问题(公共子串问题O(
n
2
m
n^2m
n2m))
主要思想
它利用之前已经部分匹配这个有效信息,假如已经在下一个位置匹配失败,那就不用移动原先起点再重新匹配,直接让f串后移,令f串中尽量多字符在已匹配部分中某个位置 到 已匹配部分的最后一个位置 完整匹配,然后从上次匹配失败的位置上继续匹配。
匹配部分
恩,首先是有子串f和主串s,黑色部分是已经匹配成功了的部分。
但下一个字符f和s就不匹配了,这时候,暴力的做法是将f重置,从这一次起点的下一个位置再进行匹配。但KMP的做法是,找出原黑色部分的最大相等前后缀,然后将前缀移动到后缀的位置,再继续匹配。如图所示,其中黑色部分是不需要重新匹配的相同部分,直接从黑色的下一个位置开始匹配。
为什么要取最长的前后缀呢?
假如选择一个长度较小的相等前后缀,那显然我们s中起点移动的步数k就会比选择最长相等前后缀的k要大,就会直接略掉最大的这种可能,但没准正确匹配就是从选择最大的开始呢?
CODE
j = 0
i = -1
for (; j < m; j++) {
while (N[i+1] != M[j] && i >= 0) i = max[i]; //如果不匹配,那就回到最大前后缀中前缀的结尾位置。
if (N[i+1] == M[j]) i++ //如果匹配就黑色部分长度+1
if (i == n-1) {match.push(j-i); i = max[i]} //继续匹配
}
最长相等前后缀的计算
定义
max(m)指f的前m个字符中最长相等前后缀
max函数求法
假设我们已经知道了max[i-1]的值,前缀=后缀,现在要在原来的基础上再加第i个位置f[i]。
若f[max[i-1]+1]=f[i],那显然就直接拼上去,max[i]=max[i-1]+1;
那要是不等于呢!?
那合并后的新前缀肯定是从原前缀中再取一个前缀+f[i]
同理,新后缀肯定是从原后缀中取一个后缀再+f[i]。
那我们再对原前后缀做一个最大相等前后缀,然后取原前缀的前缀和原后缀的后缀,显然他们是相同的。
那又为什么要长度最长呢? 与前面同理。
即这样
图打错了,next[i-1]替换为max[i-1].
然后就判断一下新前缀的下一个是否等于f[i],如果不等那么在新前后缀的基础上重复上述分割步骤,直到长度为0。
j:=max[i-1];//即原前缀最后位置
while (j>0)and(f[i]<>f[j+1]) do j:=max[j]{将原前缀变为新前缀};
其实这样就已经包括了
f[max[i-1]+1]=f[i],那显然就直接拼上去,max[i]=max[i-1]+1;
的情况了,所以删去那一步。
好了,现在我们就愉快的得出了max数组的值
CODE
i = -1
for (; j < n; j++) {
i = next[j-1]
while (N[i+1] != N[j] && i >= 0) i = next[i]
if (N[i+1] == N[j]) next[j] = i+1
else next[j] = -1
}
有没有感觉和上面匹配部分的代码神似? 其实就是一个自我匹配的过程。
时间复杂度分析
分析求max的那一段,流程为:
for i=2…m
… 如果当前位置无法与f串下一位置匹配,则将f串位置后移;
… 如果现在可以匹配了就将f已匹配位置+1
… 判断是否匹配完毕
显然在整个循环中,已匹配位置最多增加m-1次(因为只有m-1次循环啊笨)
那每一次while将f串位置后移,已匹配位置最少也会减去1 (aaaaa的情况,一次后移一位)
那已匹配位置不就是最多减去m-1次1,也就是while循环最多执行m-1次。
所以这整个for循环的的时间复杂度就是m-1次for循环+m-1次while循环也就是O(m)。
匹配部分也同理,最多O(n),所以整个时间复杂度就是O(n+m)
完整CODE
var
p,s:ansistring;
f:array[0..5000] of longint;
i,j,k:longint;
procedure getf;
var j:longint;
begin
f[0]:=-1;
f[1]:=0;j:=0; i:=1;
for i:=2 to length(p) do
begin
while (j>0)and(p[i]<>p[j+1]) do j:=f[j];
if (p[i]=p[j+1]) then inc(j);
f[i]:=j;
end;
end;
begin
readln(p); readln(s);
getf();
j:=0;
for i:=1 to length(s) do
begin
while (p[j+1]<>s[i])and(j>0) do j:=f[j];
if p[j+1]=s[i] then inc(j);
if j=length(p) then
begin
writeln(i-j+1);
break;
end;
end;
end.
扩展kmp
设 e x t [ i ] ext[i] ext[i]为主串S与子串T中,S的后缀s[i,n]与T的最长前缀长度。
目的
求出ext[1…n]
需要用到的内容
nxt[i]表示,子串T本身与子串T的一个后缀t[i…n]的最长前缀长度。
思想
与kmp大致相同,先通过自我匹配求出nxt,再去求ext,建议前置manacher。
具体算法
顺序求解每一个
e
x
t
[
i
]
ext[i]
ext[i],对于当前的i,我们求一个
m
x
=
m
a
x
(
i
+
e
x
t
[
i
]
−
1
)
mx=max(i+ext[i]-1)
mx=max(i+ext[i]−1),也就是s串中最靠后的匹配成功位置,设这个位置是从x开始匹配达到的。
然后,因为
s
[
x
.
.
m
x
]
s[x..mx]
s[x..mx]这一段与
t
[
1..
e
x
t
[
x
]
]
t[1..ext[x]]
t[1..ext[x]]匹配,
那么
s
[
i
.
.
m
x
]
s[i..mx]
s[i..mx]这一段也就是
t
[
i
−
x
+
1..
e
x
t
[
x
]
]
t[i-x+1..ext[x]]
t[i−x+1..ext[x]]
(读者可以画个图理解一下,这里就不放图了)
那么,我们只需要知道
n
x
t
[
i
−
x
+
1
]
nxt[i-x+1]
nxt[i−x+1],也就是
t
[
1..
l
e
n
t
]
t[1..lent]
t[1..lent]与
t
[
i
−
x
+
1..
l
e
n
t
]
t[i-x+1..lent]
t[i−x+1..lent]的公共前缀长度,
就有
e
x
t
[
i
]
=
n
x
t
[
i
−
x
+
1
]
.
ext[i]=nxt[i-x+1].
ext[i]=nxt[i−x+1].
其意义也就是,
s
[
i
.
.
m
x
]
与
t
[
1..
n
x
t
[
i
−
x
+
1
]
]
s[i..mx]与t[1..nxt[i-x+1]]
s[i..mx]与t[1..nxt[i−x+1]]匹配。
但还有一种情况,也就是nxt[i-x+1]是满的。那么我们无法得知,在S串的mx位置与T串的nxt[i-x+1]位置后,是否能继续匹配。那么我们可以暴力匹配并更新mx,这样保证mx是递增的,所以最终的时间复杂度是 O ( l e n s ) O(lens) O(lens)
对于nxt的求解,与上述匹配方法类似,所以只作简单的流程叙述。
首先求出nxt[1…i-1],现在求解nxt[i],同样记最大匹配位置为mx,达到这个匹配的位置是x
那么i…mx这一段与i-x+1…nxt[x]是相同的。那么同理我们有nxt[i-x+1]=nxt[i]了。不要忘记扩展与更新mx.
#include <cstdio>
#include <iostream>
#include <cstring>
#define min(a,b) ((a)>(b)?(b):(a))
#define maxn 1000010
using namespace std;
int next[maxn],bz[maxn],n,mx,mxf,mans,fr;
char c[maxn];
void out() {
for (int i=1; i<=n; i++) {
if (next[i]==0) continue;
for (int j=1; j<=next[i]; j++) cout<<c[j];
cout<<endl;
for (int j=1; j<=next[i]; j++) cout<<c[i+j-1];
cout<<endl<<endl;
}
}
int main() {
scanf("%s",c+1); n=strlen(c+1);
for (int i=2; i<=n; i++) {
if (c[i]!=c[i-1]) break;
next[2]=i-1;
}
bz[next[2]]=1;
next[1]=n;
mxf=2;
mx=1+next[2];
for (int i=3; i<=n; i++) {
if (i>mx) {
for (int j=1; j<=n; j++) {
if (c[i+j-1]!=c[j]) break;
next[i]=j;
if (i+next[i]-1!=n) bz[next[i]]=1;
}
mx=i+next[i]-1;
mxf=i;
} else {
int p=i-mxf+1;
next[i]=min(next[p],next[mxf]-p+1);
if (i+next[i]-1!=n) bz[next[i]]=1;
if (next[p]==mx) {
for (int j=next[i]+1; j<=n; j++) {
if (c[j]!=c[i+j-1]) break;
next[i]++;
if (i+next[i]-1!=n) bz[next[i]]=1;
}
mx=i+next[i]-1;
mxf=i;
}
}
}
//out();
//for (int i=1; i<=n; i++) if (i+next[i]-1!=n) bz[next[i]]=1,(cout<<next[i]<<endl);
bz[0]=0;
for (int i=1; i<=n; i++) {
//cout<<i<<" "<<i+next[i]-1<<" "<<bz[next[i]]<<endl;
if (i+next[i]-1==n && bz[next[i]]) mans=max(mans,next[i]);
}
if (mans==0) {
cout<<"CD can't remember anything."<<endl;
return 0;
}
for (int i=1; i<=mans; i++) printf("%c",c[i]);
}