虽然入ACM大坑也两年了吧emmmm但是感觉一直没怎么努力做题。。就连KMP我也是昨天才学会。。惭愧。
KMP算法是一种高效率的字符串匹配算法,其复杂度近似于O(n),主要用于在一个长度为n的字符串s中,判断是否存在一个长度为m的给定子串t
在讲KMP算法之前,我们先来了解一下普通的暴力算法要如何解决这个问题。
我们想在一个长字符串(称为母串)中寻找一个子串,我们可以枚举母串中所有的长为t的子串(t为子串的长度),然后再比较这两个子串是否相等,若相等即可返回结果。
代码如下:
for(int i = 0; i < strlen(s) - strlen(t); i++) {
//i表示子串的起始位置
int j;
for(j = 0; j < strlen(t); j++)//逐个比较需要寻找的子串中的第j个字符
if(s[i+j] != s[j]) break;//匹配失败,跳出循环
if(j == strlen(t)) return i;//匹配成功,返回子串所在位置
}
return -1;//循环结束,未找到子串
s = “ababaababb”
t = “ababb”
我们要在s中寻找t,首先将s中第一个长为4的子串与t比较
(‘x’表示匹配失败,”.’表示未匹配,’√’表示匹配成功)
母串 | a | b | a | b | a | a | b | a | b | b |
---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | |||||
比较结果 | √ | √ | √ | √ | × |
第一次匹配失败了,然后我们将s中第二个子串与t比较,即相当于将t右移一位与s比较
母串 | a | b | a | b | a | a | b | a | b | b |
---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | |||||
比较结果 | × | . | . | . | . |
显然第二次匹配又失败了,然后继续与第三个字符串比较
母串 | a | b | a | b | a | a | b | a | b | b |
---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | |||||
比较结果 | √ | √ | √ | × | . |
第四次比较:
母串 | a | b | a | b | a | a | b | a | b | b |
---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | |||||
比较结果 | × | . | . | . | . |
接着继续比较第五次
母串 | a | b | a | b | a | a | b | a | b | b |
---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | |||||
比较结果 | √ | × | . | . | . |
第六次
母串 | a | b | a | b | a | a | b | a | b | b |
---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | a | b | b | |||||
比较结果 | √ | √ | √ | √ | √ |
匹配成功,此时返回i的值为5(从0开始计数)
总的过程如下:
母串 | a | b | a | b | a | a | b | a | b | b | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|
第一次比较 | a | b | a | b | b(失配) | × | |||||
第二次比较 | a(失配) | b | a | b | b | × | |||||
第三次比较 | a | b | a | b(失配) | b | × | |||||
第四次比较 | a(失配) | b | a | b | b | × | |||||
第五次比较 | a | b(失配) | a | b | b | × | |||||
第六次比较 | a | b | a | b | b | √ |
通过4次枚举子串的起始位置,我们找到了这个子串。
我们知道,母串s中长为t的字符串的个数共有|s|-|t|个,每次判断一个子串是否能满足匹配需要至多|t|次,记n=|s|,m=|t|,则这个算法的复杂度为O(mn)
但是,我们发现,有很多次比较是多余的
当我们比较完第一次时,发现第一次在匹配第5个字符的时候匹配失败(简称失配),这时我们就已经知道母串的前四个字符与需要查找的子串相同,即前四个字符为abab
那么,显然我们第二次比较就是多余的。因为母串的前四个字符为abab,其第二个字符b与子串的首字符a不同。
同样,当我们经过第三次比较之后,也就知道母串的第3-5个字符为aba(因为匹配第四个字符时才失配)
所以第四次比较也是多余的。
我们已经知道,上一次匹配带给我们的信息,足以让我们对接下来的几次匹配进行一些筛选,去掉那些不可能匹配上的选择。
但是怎么确定每次匹配之后下次需要匹配的位置呢?
这就要我们首先思考一下,在上面的例子中,为什么第一次匹配在第五个位置失配后,第二次匹配我们可以跳过。
因为我们的目的是要在母串中找到一个子串,而在第一次匹配过程中,我们已经知道了母串的部分信息——前四个字符与子串相同,即abab。所以在母串中的相应位置若和子串首部的序列不能匹配(母串的第二个字符为b,与子串的首字母a不匹配),那么我们就可以跳过第二次匹配。而第三个字母a与子串的首字母a匹配了,故可以从这个位置继续进行匹配。
因为我们的“跳过”,是因为在某次匹配失败后知道了母串的某些信息,从而根据这些信息可以跳过这些匹配。
在第k次匹配失败后,我们得到的母串的信息一定是:有一段与子串的前k-1个字符相同的子串
从母串中获得的信息是由子串决定的,所以是否可以跳过也是由子串决定的
子串决定是否可以跳过,那又是怎么决定可以跳过之后从哪里开始匹配呢??
首先需要介绍两个概念:前缀和后缀
从字面意思上说,前缀就是从前面开始数,后缀就是从后面开始数
例如:有一个整数串,为:1,2,3,4,5,6,7,8,9,10
那么这个串长为3的前缀为1,2,3;长为4的后缀为7,8,9,10
以这个串第6个位置为结束,长为2的前缀为1,2;长为3的后缀为4,5,6
当我们在第k个位置失配后,我们需要找的下一个可能的匹配所在的位置,就恰好是【以第k个位置结束,相同的最长前缀和后缀的长度(在这里认为前缀和后缀不取[0,k-1]这个序列)】
可能有点绕口,为了更直观的理解,我们再来一个例子。
举例之前说明一下,因为我们已经知道,决定能否跳过的只有子串,所以这里不假设母串的信息,只假设在第i次匹配时,子串在第k个位置失配。而且我们研究的主要是,跳过之后开始匹配的位置,所以只列举出第一次匹配,以及下一次不可以跳过的匹配。
母串 | a | b | c | f | a | b | c | f | . | . | . | . | 能否跳过 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
第i次比较 | a | b | c | f | a | b | c | e(失配) | 否 | ||||
第i+1次比较 | a(失配) | b | c | f | a | b | c | e | 能 | ||||
第i+2次比较 | a(失配) | b | c | f | a | b | c | e | 能 | ||||
第i+3次比较 | a(失配) | b | c | f | a | b | c | e | 能 | ||||
第i+4次比较 | a | b | c | f(匹配成功) | a(继续匹配之后的母串) | b | c | e | 否 |
在这个例子中,进行完第i次比较后,我们可以跳过子串的前3个字符的比较
我们注意到以第k-1个位置为结束的前缀和后缀(k为失配的位置,在例子中为8),发现长度为3的前缀为abc,长度为3的后缀也为abc,前缀和后缀相等(长度为7的前缀和后缀也相同,但是我们不考虑[0,k-1]这个前/后缀)。故在第k个位置失配后,可以直接从第4个字符开始比较,即跳过子串的前3个字符。
通过之前的分析,我们已经知道:
在第k个位置失配后,我们由子串可以得到母串的一些信息
由子串(获得的信息)可以决定,在第k个位置失配后能否跳过接下来的几次匹配
由第k个位置的前缀和后缀可以决定跳过匹配后,下一个需要匹配的位置
我们记f[k]表示在第k个位置失配时,下一次匹配的位置
由上面的信息可以知道:f[k]的值仅由待匹配的子串t来决定,而与母串无直接关联。且f[k]的值恰好等于以k为结尾(不包括第k个字符)时,前缀和后缀完全相同时的最大长度(在这里认为前缀和后缀不取[0,k-1]这个序列)。
让我们尝试求一下以下字符串的失配函数:
s = “ababaaababa”
首先,我们令f[0]=f[1]=0,因为无法找到他们满足条件的前缀和后缀。然后从f[2]开始。
s[2]的前缀有”a”,后缀有”b”,没有相同的,故最大长度为0
s[3]的前缀有”a”,”ab”,后缀有”a”,”ba”,最长相同的前缀后缀为”a”,长度为1,故f[3]=1
s[4]的前缀有”a”,”ab”,”aba”,后缀有”b”,”ab”,”bab”,其中”ab”相同,f[4]=2
s[5]的前缀有”a”,”ab”,”aba”,”abab”,后缀有”a”,”ba”,”aba”,”baba”,相同的前后缀有”aba”,长度为3,f[5]=3
s[6]的前缀有”a”,”ab”,”aba”,”abab”,”ababa”,后缀有”a”,”aa”,”baa”,”abaa”,”babaa”,最长相同前后缀为”a”,f[6]=1
s[7]的前缀有”a”,”ab”,”aba”,”abab”,”ababa”,”ababaa,后缀有”a”,”aa”,”aaa”,”baaa”,”abaaa”,”babaaa”,最长相同前后缀为”a”,f[7]=1
同理,k=8时,相同的前后缀为”ab”,f[8]=2。k=9时,相同的前后缀为”aba”,f[9]=3。k=10时,相同的前后缀为”abab”,f[10]=4
所以我们最后得到的f数组为:
s | a | b | a | b | a | a | a | b | a | b | a |
---|---|---|---|---|---|---|---|---|---|---|---|
k | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
f[k] | 0 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 | 4 |
相等的前后缀 | “” | “” | “” | “a” | “ab” | “aba” | “a” | “a” | “ab” | “aba” | “abab” |
再重复一下f数组的含义:f[k]表示在第k次匹配失配时,我们无需从头开始匹配子串,而是可以直接从子串的第f[k]个字符开始继续匹配
先来回顾一下我们暴力算法
for(int i = 0; i < strlen(s) - strlen(t); i++) {
//i表示子串的起始位置
int j;
for(j = 0; j < strlen(t); j++)//逐个比较需要寻找的子串中的第j个字符
if(s[i+j] != s[j]) break;//匹配失败,跳出循环
if(j == strlen(t)) return i;//匹配成功,返回子串所在位置
}
return -1;//循环结束,未找到子串
而采用了KMP算法后,我们不再需要枚举母串中所有长为m的子串并一一匹配,而是采取逐个字符匹配的方法。
如果s[i] == t[j],那么这意味着我们已经匹配好了子串中的前j个字符
如果s[i] != t[j],说明我们在匹配子串的第j位时失配了,根据KMP算法的思想,我们可以令j=f[j],继续判断s[i]是否和新的t[j]相等。
转换为代码:
int j = 0;//初始时匹配的是子串的第j个字符
for(int i = 0; i < strlen(s); i++) {
//while中条件j是为了防止死循环,然后判断第j位是否失配,若失配,则继续匹配第f[j]个字符
while(j && s[i] != t[j]) j = f[j];
//结束while时可能是由于匹配成功而退出,也有可能是由于j=0退出,在此讨论
if(s[i] == t[j]) j++;//如果是第j位匹配成功,那么匹配下一位
if(j == m) return i-m+1;//整个子串匹配完成,返回子串开始的位置
}
return -1;//循环结束,未找到子串
为什么先讲KMP的主体部分再讲预处理部分呢?因为预处理部分的计算方法和主体部分非常相似,而且先理解主体部分的思想有助于理解预处理部分的计算过程。
再回顾一下失配函数的定义:f[k]表示以在第k个位置失配时,下一次匹配的位置。下一次匹配的位置正是以k结尾的最长相同前后缀的长度。
具体该怎么求失配函数呢?首先f[0]=f[1]=0,因为我们无法找到对应的前后缀,故也不存在相同的前后缀。
然后,我们要寻找相同的前缀和后缀,那么,我们可以用类似找子串的方式,将子串本身向右平移,使得平移后的前缀和原来的后缀在同一个位置上,再和原来的子串进行比较,看重合的位数有多少,即说明公共的前后缀的长度有多少。既然是比较重合的位数,自然我们也不用考虑溢出的部分。
我们将原子串即为t,平移i位的子串记为 ti t i
k | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 备注 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
原子串 | a | b | a | b | a | a | a | b | a | b | a | |
平移1位 | a | b | a | b | a | a | a | b | a | b | t[1]≠t1[0],f[2]=0 t [ 1 ] ≠ t 1 [ 0 ] , f [ 2 ] = 0 | |
平移2位 | a | b | a | b | a | a | a | b | a | t[2]=t2[0],f[3]=1 t [ 2 ] = t 2 [ 0 ] , f [ 3 ] = 1 | ||
平移2位 | a | b | a | b | a | a | a | b | a | t[3]=t2[1],f[4]=2 t [ 3 ] = t 2 [ 1 ] , f [ 4 ] = 2 | ||
平移2位 | a | b | a | b | a | a | a | b | a | t[4]=t2[2],f[5]=3 t [ 4 ] = t 2 [ 2 ] , f [ 5 ] = 3 | ||
平移2位 | a | b | a | b | a | a | a | b | a | t[5]≠t2[3],f[6]=0 t [ 5 ] ≠ t 2 [ 3 ] , f [ 6 ] = 0 | ||
平移3位 | a | b | a | b | a | a | a | b | t[3]≠t3[0],f[4]=max(f[3]+1,0)=2 t [ 3 ] ≠ t 3 [ 0 ] , f [ 4 ] = m a x ( f [ 3 ] + 1 , 0 ) = 2 | |||
平移4位 | a | b | a | b | a | a | a | t[4]=t4[0],f[5]=max(f[4]+1,1)=3 t [ 4 ] = t 4 [ 0 ] , f [ 5 ] = m a x ( f [ 4 ] + 1 , 1 ) = 3 | ||||
平移4位 | a | b | a | b | a | a | a | t[5]=t4[1],f[6]=0 t [ 5 ] = t 4 [ 1 ] , f [ 6 ] = 0 | ||||
平移5位 | a | b | a | b | a | a | t[5]=t5[0],f[6]=1 t [ 5 ] = t 5 [ 0 ] , f [ 6 ] = 1 | |||||
平移5位 | a | b | a | b | a | a | t[6]≠t5[1],f[7]=0 t [ 6 ] ≠ t 5 [ 1 ] , f [ 7 ] = 0 | |||||
平移6位 | a | b | a | b | a | t[6]=t6[0],f[7]=1 t [ 6 ] = t 6 [ 0 ] , f [ 7 ] = 1 | ||||||
平移6位 | a | b | a | b | a | t[7]=t6[1],f[8]=2 t [ 7 ] = t 6 [ 1 ] , f [ 8 ] = 2 | ||||||
平移6位 | a | b | a | b | a | t[8]=t6[2],f[9]=3 t [ 8 ] = t 6 [ 2 ] , f [ 9 ] = 3 | ||||||
平移6位 | a | b | a | b | a | t[9]=t6[3],f[10]=4 t [ 9 ] = t 6 [ 3 ] , f [ 10 ] = 4 | ||||||
平移7位 | a | b | a | b | t[7]≠t7[0],f[8]=max(f[7]+1,0)=2 t [ 7 ] ≠ t 7 [ 0 ] , f [ 8 ] = m a x ( f [ 7 ] + 1 , 0 ) = 2 | |||||||
平移8位 | a | b | a | t[8]=t8[0],f[9]=max(f[8]+1,1)=3 t [ 8 ] = t 8 [ 0 ] , f [ 9 ] = m a x ( f [ 8 ] + 1 , 1 ) = 3 | ||||||||
平移8位 | a | b | a | t[9]=t8[1],f[10]=f[9]+1=4 t [ 9 ] = t 8 [ 1 ] , f [ 10 ] = f [ 9 ] + 1 = 4 | ||||||||
平移9位 | a | b | t[9]≠t9[0],f[10]=max(f[9]+1,0)=4 t [ 9 ] ≠ t 9 [ 0 ] , f [ 10 ] = m a x ( f [ 9 ] + 1 , 0 ) = 4 | |||||||||
f[k] | 0 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 | 4 |
看懂了吗?我们将待匹配的串t进行平移,再和原来的t进行比较,以平移的串为基准,找到原串中与其重合的部分,这个重合的部分就对应原串中某个位置的相同的前缀和后缀,从而可以更新我们的f数组。转换为代码为:
f[0] = f[1] = 0;
for(int i = 1; i < m-2; i++) {
//平移的位数
int j = 0;//与原串的第j位比较
while(i+j < m && t[i+j] == t[j]) {
f[i+j+1] = max(f[i+j+1], f[i+j]+1);//当第i+j位匹配成功时,第i+j+1位所具有的最长前后缀的长度,即为第i+j位的最长前后缀长度+1
j++;//继续判断下一位是否能匹配成功
}
}
有没有发现,这种方法和我们刚开始谈到的暴力找子串的方法有点类似?他们都是将要找的部分逐个平移,然后判断是否找到或是否需要更新。
如果上面的KMP主体部分你已经理解了的话,你很容易想到,这样逐个平移会造成和暴力一样的结果——产生很多次不必要的比较。
比方说,在平移2位时,我们找到了长度为3的前后缀,但在比较t[5]和t2[3]时失配了,然后我们是不是一定要讨论平移3位的情况呢?
其实没必要,因为此时我们已经更新过f[5]的值了,且f[5]=3也就意味着我们已经知道在5的位置上有相同的长度为3的相同前后缀。即t[0-2]和t[2-4]是相同的。虽然我们匹配t[5]失败了,但是我们可以考虑在t[2-4]内有没有已经知道的后缀能与前缀匹配,如果有的话,那我们直接将前缀移过来就可以了。而t[0-2]和t[2-4]相同,f[3]刚好记录的就是t[0-2]中的最长相同前后缀的长度。
换句话说,如果f[3]=l,说明在0-2内有一个长为l的后缀与长为l的前缀相同,又因为f[5]=3,说明t[0-2]和t[2-4]是相同的,因此t[2-4]内也同样有一个长为l的后缀与前缀相同,基于KMP的思想,我们就可以直接将这个前缀移过来。移过来之后我们需要比较的元素就是t[3]和t[5]了。
这个过程用文字表述起来确实很难理解,所以我在这里也花费了不少时间来举例子,如果实在难以理解的话可以反复看我的例子。
即用KMP思想优化后,以上找子串的过程为:
k | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 备注 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
原子串 | a | b | a | b | a | a | a | b | a | b | a | |
平移1位 | a | b | a | b | a | a | a | b | a | b | t[1]≠t1[0],f[2]=0 t [ 1 ] ≠ t 1 [ 0 ] , f [ 2 ] = 0 | |
平移2位 | a | b | a | b | a | a | a | b | a | t[2]=t2[0],f[3]=1 t [ 2 ] = t 2 [ 0 ] , f [ 3 ] = 1 | ||
平移2位 | a | b | a | b | a | a | a | b | a | t[3]=t2[1],f[4]=2 t [ 3 ] = t 2 [ 1 ] , f [ 4 ] = 2 | ||
平移2位 | a | b | a | b | a | a | a | b | a | t[4]=t2[2],f[5]=3 t [ 4 ] = t 2 [ 2 ] , f [ 5 ] = 3 | ||
平移2位 | a | b | a | b | a | a | a | b | a | t[5]≠t2[3],j=f[3]=1 t [ 5 ] ≠ t 2 [ 3 ] , j = f [ 3 ] = 1 ,将t[j]移动到和t[5]对齐的位置 | ||
平移4位 | a | b | a | b | a | a | a | t[5]≠t4[2],j=f[1]=0 t [ 5 ] ≠ t 4 [ 2 ] , j = f [ 1 ] = 0 ,将t[0]移动到和t[5]对齐的位置 | ||||
平移5位 | a | b | a | b | a | a | t[5]=t5[0],f[6]=1 t [ 5 ] = t 5 [ 0 ] , f [ 6 ] = 1 | |||||
平移5位 | a | b | a | b | a | a | t[6]≠t5[1],j=f[1]=0 t [ 6 ] ≠ t 5 [ 1 ] , j = f [ 1 ] = 0 ,将t[0]移动到和t[6]对齐的位置 | |||||
平移6位 | a | b | a | b | a | t[6]=t6[0],f[7]=1 t [ 6 ] = t 6 [ 0 ] , f [ 7 ] = 1 | ||||||
平移6位 | a | b | a | b | a | t[7]=t6[1],f[8]=2 t [ 7 ] = t 6 [ 1 ] , f [ 8 ] = 2 | ||||||
平移6位 | a | b | a | b | a | t[8]=t6[2],f[9]=3 t [ 8 ] = t 6 [ 2 ] , f [ 9 ] = 3 | ||||||
平移6位 | a | b | a | b | a | t[9]=t6[3],f[10]=4 t [ 9 ] = t 6 [ 3 ] , f [ 10 ] = 4 | ||||||
f[k] | 0 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 | 3 | 4 | f计算完毕 |
这样找子串,我们就不需要逐个平移,而是在更新f数组的过程中,同时也在调用f数组内的值,借用f数组内的值来舍弃掉许多没有必要的平移。
转换为代码为:
f[0] = f[1] = 0;//初始化
int j = 0;//刚开始比较的是0位
for(int i = 1; i < m-1; i++) {
//平移i位的串与原串做比较
//while中的j防止死循环,不判断j=0时的情况
while(j && t[i] != t[j]) j = f[j];//跳过比较,直接将第f[j]位移动到第i位继续进行比较
//j=0的情况在这里考虑
if(t[i] == t[j]) j++;//第j位匹配成功,准备匹配下一位
f[i+1] = j;//已经为第i+1个字符找到了一个长为j的相同前后缀
}
怎么样?这个代码和KMP的主代码是不是有异曲同工之妙呢?
这两段代码本来也长得非常相似,所以一定要注意他们的区别哦
KMP算法主要用于在一个母串中寻找子串
暴力寻找子串的方法复杂度为O(mn),其中包含了大量多余的匹配过程。而KMP算法利用每次匹配失配的位置,可以确定母串中一小段内容与子串相同,通过这些信息可以跳过一些多余的比较。而跳过之后所需要进行的下一次比较的位置,由我们的失配函数f来决定,而f[k]的值即为以k为结束的最长相同前后缀的长度。每次在第k个位置失配后,可以直接跳到第f[k]个位置继续进行比较,而中间的部分是可以直接排除的。
失配函数是由待匹配串本身的构造而决定的,求失配函数的过程相当于是【在失配函数中用KMP算法找失配函数】,所以找失配函数的代码与KMP主过程的代码非常相似。
虽然有一些玄学的影响,但是我们普遍认为KMP的复杂度是O(n)级别的,但是当子串无重复或重复片段非常少时,KMP算法也会退化到O(mn)
KMP算法是算法竞赛中常见的算法,其变化不但包括找字符串的子串,也可以找整数的子串。不但可以判断子串是否存在,还能判断子串在母串中出现的最大长度。还有一些其他的变形,所以热衷于算法竞赛的童鞋一定要学会KMP算法哦~
最后开个车,KMP算法有个很厉害的名字————————————(KanMaoPian)
这不是去幼儿园的车,我要下车!