KMP算法
算法简介
KMP算法解决的问题主要在于如何实现在一个较长的字符串中匹配得到一个较短的字符串。
我们知道对于子串匹配的问题,常用的暴力解法是不断枚举起点,并反复遍历字符串以确定是否匹配,而KMP算法则可以简化这一个过程。该算法可以自动跳过一些可能会导致重复匹配的字符串从而减少匹配的次数。
算法详解
首先我们假设我们待匹配的字符串是s = abababcafc
而子串为p = ababc
,整个匹配过程如下所示
1 |
|
我们可以发现与暴力匹配不同,字符串直接向后移动了两位然后完成了匹配,我们可以观察第二个字符串存在这样的性质
1 |
|
我们可以发现在这个串中1、2、3三个字符串均相同,实际上这里就是KMP移动字符串的依据,在第一次匹配的过程中我们发现字符串0-1
位和2-3
位相同,而第一次又成功匹配了0-3
位的字符串,所以说实际上下一次进行匹配的时候我们的0-1
位可以直接移动到当前2-3
位的位置上,然后从2
位重新开始匹配,从而减少了匹配的次数。
而此时我们便会面临一个问题,如何获得上面我们需要的这种相同的关系,此时我们可以考虑定义一个数组next[]
,假设next[j]
表示以j
在p
串中以j
结尾的满足其前缀与后缀相同子串中前缀结尾的索引,而后续匹配时的跳转操作可以直接使用这个数组完成跳转。接下来我们可以尝试来计算这个数组。
next数组的计算方法
为了推导方便,此时我们假设p=ababdababaa
,实际使用的时候我们只需要知道如何从next[0~i - 1]
推得next[i]
即可
1 |
|
但是我们面临着一个问题,如果第一次匹配不成功应该怎么解决,我们假设下面一种情况
1 |
|
为了求解得到next[10]
,我们只能退而求其次,去尝试更短的子串看看是否可以求解。根据next[9]=4
我们可以知道p[1:4] == p[6:9]
,而我们又可以发现next[next[9]] = next[4] = 2
,即p[1:2] == p[3:4]
,而根据next[9]
的定义我们又知道p[3:4] == p[8:9]
,同时p[next[next[9]] + 1] == p[10]
,所以我们可以得到p[1:3] == p[8:10]
即next[10] = 3
。
而上面的推导存在一个边界,即当next[next[....]] = 0
时,此时不再指望next[0~n-1]
会对next[n]
做出任何贡献。
next数组构造代码实现
1 |
|
匹配过程
既然此时我们获得了next[]
,我们便可以开始尝试进行匹配s
和p
了,匹配过程与next[]
构造的过程类似,即在出现第一个无法匹配即s[i] != p[j]
或者匹配完全时,将会尝试最大程度的向前移动,这里我们还是继续假设待匹配的字符串是s = ababababfab
而子串为p = ababf
,匹配的过程可以像这样进行
1 |
|
此时我们要开始找最长的可以移动的距离,我们可以发现s[5] == p[3]
,而同时存在p[1:2] == p[3:4]
,故此时我们可以作如下的移动
1 |
|
可以看到这样我们就成功实现了利用next[]
一次性移动多位的操作。这里的思想与求next[]
的过程类似,即我们实际上是在s
上搜索一个与当前已知前缀相互匹配的后缀,假设存在任何一个可以相互匹配的前后缀即可直接将字符串开头移动到求得的后缀的开头的位置。
匹配过程代码实现
1 |
|
KMP算法的应用
状态机+KMP(AcWing 1052. 设计密码)
该题目中实际上可以将我们需要求得的密码视作n
次状态转换,其中第一个状态即state[0]
就是不存在任何匹配的状态,而由于我们的要求是不能匹配子串,假设子串长度为m
,故我们要求不能到达state[m]
。
由于密码的每一位都存在26种情况(题目中密码组成是全小写字母),故对于每一个状态均存在26种转移方式。而我们又知道对于KMP算法实际上每次在计算匹配情况的时候其都只会关心待匹配串s
的一个字母,根据这种性质我们可以利用KMP的匹配方式求得每种状态最远可以匹配的子串长度,只要求得的长度没有到达n
那就是一种可用的密码。
首先我们同样可以计算出KMP所需的next[]
,然后可以进行状态转移
1 |
|