Index
KMP算法
KMP算法的作用
KMP算法是由三个首字母分别为K,M,P的计算机学家发明的。
主要是拿来解决以下问题的:
有一个长度为M的字符串A和一个长度为N的字符串B。问B这个字符串完整地出现在了A字符串的哪个位置。
当然,一般来说,M远大于N,不过要是不远大于也未尝不行。
引入几个专业的名称,称A串为文本串,B串为模式串,也有称为母串子串、称A为主串,称B为关键字的。
那么KMP的过程就是我们常说的字符串匹配,一种关键字查询,的过程。
举例:
文本串:
那么KMP的过程就是我们常说的字符串匹配,一种关键字查询,的过程。
模式串:
过程
结果:
“过程”一词在文本串中 [ 6 , 7 ] [6,7] [6,7]和 [ 30 , 31 ] [30,31] [30,31]出现。
大概就这样。
这个过程叫字符串匹配,我越说越糊涂了。
字符串匹配的朴素算法
很容易的想到 Θ ( M N ) \Theta(MN) Θ(MN)的算法:
for(int i=0;i<M;i++)
{
bool flg=0;
for(int j=0;j<N && !flg;j++)
if(A[i+j]!=B[j]) flg=1;
if(flg) {/*记录答案*/}
}
可以理解为枚举模式串的起点,然后一位位的枚举看接下来的N个字符是不是一样的。
大概是这样的:
也可以理解为将模式串摆在文本串的下面,开始比较,如果匹配失败,则将模式串后移一位,再次比较。
这样理解也可以写出如下代码:
for(int i=0,j=0;i<n;i++)
{
if(j==m) {/*记录答案并回溯*/}
if(A[i]!=B[j]) /*回溯*/ i=i-j+1,j=0;
else i++,j++;
}
大概是这样吧。
试图优化朴素算法
我说,如果失配(匹配失败),非要“将模式串后移一位”吗?
比方说:
为什么呢?
不妨这么想:
假设你就是模式串,你只看得到你所经过的文本串。就像这样:
你发现,至少自己和文本串的前4个字符是不匹配的,剩下的,不知道,接下来要往前走几步呢?
\endline
答案很显然,当然是选择方案二——因为刚刚写了better!
唔姆(微笑掩饰着尴尬的表情)……
答案是——
如果采用方案二,你知道,至少你的第一位是匹配的,这里才有可能成功匹配,如果往后走一步,那么肯定是不匹配的,浪费时间。
那么,总之,kmp算法就是,在朴素字符串匹配的基础上,通过某种方案,来让每次失配后模式串最有效率地移动。
也可以说是对于之前给出的代码2,回溯时,i不变,j减小到某个合适的值。
KMP算法
字符串最长公共前后缀
字符串的前后缀
这个……
随便举个例子就过了吧:
字符串:%%%sdltqlwsl%%%
(圣诞了,天气冷,我睡了)
长度 | 前缀 | 后缀 |
---|---|---|
1 | % | % |
2 | %% | %% |
3 | %%% | %%% |
4 | %%%s | l%%% |
5 | %%%sd | sl%%% |
6 | %%%sdl | wsl%%% |
7 | %%%sdlt | lwsl%%% |
8 | %%%sdltq | qlwsl%%% |
9 | %%%sdltql | tqlwsl%%% |
10 | %%%sdltqlw | ltqlwsl%%% |
11 | %%%sdltqlws | dltqlwsl%%% |
12 | %%%sdltqlwsl | sdltqlwsl%%% |
13 | %%%sdltqlwsl% | %sdltqlwsl%%% |
14 | %%%sdltqlwsl%% | %%sdltqlwsl%%% |
15 | %%%sdltqlwsl%%% | %%%sdltqlwsl%%% |
最长公共前后缀
就是指最长的那个既是前缀又是后缀的字符串,当然不得是其本身。
比方说:
%
%
%
s
d
l
t
q
l
w
s
l
%
%
%
→
%
%
%
a
a
a
a
a
a
→
a
a
a
a
a
a
b
c
d
a
b
c
→
a
b
c
\%\%\%sdltqlwsl\%\%\% ~ \to \%\%\% \\ aaaaaa~\to~aaaaa \\ abcdabc~\to~abc
%%%sdltqlwsl%%% →%%%aaaaaa → aaaaaabcdabc → abc
之类的。
KMP算法的流程
某年(1977年),
K
K
K先生、
M
M
M先生、
P
P
P先生联合发表了一篇文章,他们认为,前文所说:“每次失配后最有效率的移动”,“
j
j
j降到某个合适的值”,移动的距离就是——假设在模式串上j号位上失配,则令
j
=
n
x
t
[
j
]
j=nxt[j]
j=nxt[j](尽管我个人喜欢用
f
a
i
l
[
j
]
fail[j]
fail[j],
f
a
i
l
[
j
]
fail[j]
fail[j]代表模式串前
j
−
1
j-1
j−1个字符的最长公共前后缀。)并给出证明以及计算
f
a
i
l
fail
fail数组的方法。
(小声BB: KaTeX \KaTeX KATEX加粗不了汉字……)
所以kmp算法大概就是这么个东西:
inline int kmp()
{
for(int i=0,j=0;;i++,j++)
{
if(j==m) return i-j+1;
if(i==n) return -1;
while(j!=-1 && a[i]!=b[j]) j=fail[j];
}
}
其中 r e t u r n − 1 ; return ~ -1; return −1;代表匹配失败, r e t u r n i − j + 1 ; return ~ i-j+1; return i−j+1;代表找到了第一次成功匹配的位置。
fail数组/nxt数组
f
a
i
l
[
j
]
fail[j]
fail[j]代表:
①第
j
j
j位失配以后,
j
j
j应该去往的地方。(看上面代码3)
②模式串前
i
i
i个字符即
b
[
0
,
1
,
⋯
,
j
−
1
]
b[0,1,\cdots,\bold{j-1}]
b[0,1,⋯,j−1]的最长公共前后缀长度。
其中 f a i l [ 0 ] = − 1 \bold{fail[0]=-1} fail[0]=−1。
正确性
为什么说移动到最长公共前后缀是有效率的?
。。。
懒得说了,比较显然吧。
如果不显然的话就是我的问题。
可计算性
假设我们已经求出了前
j
−
1
j-1
j−1个
f
a
i
l
fail
fail,现在要求
f
a
i
l
[
j
]
fail[j]
fail[j]。
对了别忘了,
f
a
i
l
[
j
]
fail[j]
fail[j]代表模式串前
i
i
i个字符即
b
[
0
,
1
,
⋯
,
j
−
1
]
b[0,1,\cdots,\bold{j-1}]
b[0,1,⋯,j−1]的最长公共前后缀长度,是
b
[
0
,
1
,
⋯
,
j
−
1
]
\bold{b[0,1,\cdots,j-1]}
b[0,1,⋯,j−1]的最长公共前后缀长度,是
b
[
0
,
1
,
⋯
,
j
−
1
]
\bold{b[0,1,\cdots,j-1]}
b[0,1,⋯,j−1]的最长公共前后缀长度。
重要的事情说三遍。——尼采《善恶的彼岸》
那么:
为什么呢?
举例举例,这里有贪心的一点点想法:
我们想要求得 0 → j − 1 0\to j-1 0→j−1的最长公共前后缀,我们希望借助前面的 f a i l fail fail,尽快求出尽量大的公共前后缀。
我们自然会想:
如果,上一个求出来的最长公共前后缀,如果这俩的后一个字符相同的话,那岂不是很开心,现在要求的最长公共前后缀就是上一个求出来的最长公共前后缀+1。
如果不相等?
就是说,这个最长公共前后缀的长度它小于
f
a
i
l
[
j
−
1
]
fail[j-1]
fail[j−1],我们要要重新找后缀了,怎么做呢?把后缀的长度--
,然后看这个长度的前后缀是不是相等的?这显然不行。那怎么办?
我们找最长公共前后缀一定是在某个之前的最长公共前后缀的长度+1得来的(-1+1=0),所以我们可以只看已经求出的
f
a
i
l
fail
fail。只有长度
<
f
a
i
l
[
j
−
1
]
\lt fail[j-1]
<fail[j−1]的
f
a
i
l
fail
fail,就有可能成为新的最长公共前后缀。于是我们就走
f
a
i
l
fail
fail链,这样就可以避免一些无用的查询了。
大家可以很明显地看出哪些是我之前写的哪些是我之后写的,认真和水一目了然。
KMP的代码
#include<cstdio>
#define maxn 1000005
#define maxm 10005
int T,n,m,a[maxn],b[maxm],fail[maxm];
inline void getfail()
{
fail[0]=-1;
for(int i=0,j=-1;i<m;fail[++i]=++j)
while(j!=-1&&b[i]!=b[j]) j=fail[j];
}
inline int kmp()
{
for(int i=0,j=0;;i++,j++)
{
if(j==m) return i-j+1;
if(i==n) return -1;
while(j!=-1&&a[i]!=b[j]) j=fail[j];
}
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
for(int i=0;i<m;i++) scanf("%d",&b[i]);
getfail();
printf("%d\n",kmp());
}
}