字符串匹配算法之KMP总结

版权声明:版权所有,转载请标明博文出处。 https://blog.csdn.net/HQG_AC/article/details/85793342

字符串匹配有很多方法,比如暴力,哈希等等,还有一种广为人知的算法 K M P ---KMP

一.问题引入

需要一种算法,能够在线性的时间内判断 a [ 1... N ] a[1...N] 是否是字符串 b [ 1... M ] b[1...M] 的字串,并要求返回字符串a在b中匹配的所有位置

思考暴力。

枚举i从 1 > m 1->m 表示 b b 匹配的左端点,然后 O ( n ) O(n) 的判断 b [ i . . . i + n 1 ] b[i...i+n-1] 是否与 a [ 1... n ] a[1...n] 匹配。

我们很容易发现这样的方法在极端数据下跑的很慢,比如:

aaaaaaaaaaaaaaaaaab
aaaaab

每次要匹配到a最后一个位置才发现不相等。时间复杂度 O ( n m ) O(nm)

当然此问题亦可以通过哈希解决,笔者在此不多赘述

接下来我们就讲神奇 K M P KMP 算法

二.算法概述

K M P KMP 算法,又称模式匹配算法,是一种能够高效,准确的处理字符串匹配的算法

KMP算法基本分为两部:

1.首先是对 A A 数组(模式串)进行自我匹配。

建立一个 n e x t next 数组, n e x t [ i ] next[i] 表示以 i i 结尾的非前缀字串与A的前缀能够匹配的最大长度。

其中“以 i i 结尾的非前缀字串”通俗的说就是非前缀的后缀,比如aab的非前缀的后缀就是 { b } , { a b } \{b\},\{ab\}

n e x t [ i ] = max { j } , j < i next[i]=\max\{j\}, j<i A [ i j + 1... i ] = A [ 1... j ] A[i-j+1...i]=A[1...j]

举个例子:

A A 串为 " a b a b a b a a c " "abababaac" ,A数组的next[7]应该为5,推导过程如下:

发现有三个可行的j满足 A [ i j + 1... i ] = A [ 1... j ] A[i-j+1...i]=A[1...j]

A [ 7...7 ] = { a } A[7...7]=\{a\} A [ 1...1 ] = { a } A[1...1]=\{a\} 匹配;
A [ 5...7 ] = { a b a } A[5...7]=\{aba\} A [ 1...3 ] = { a } A[1...3]=\{a\} 匹配;
A [ 3...7 ] = { a b a b a } A[3...7]=\{ababa\} A [ 1..5 ] = { a b a b a } A[1..5]=\{ababa\} 匹配;

其中 j j 最大的是第 3 3 个为 5 5

如何更快的计算 n e x t next 数组?

不妨设 n e x t [ 1...6 ] next[1...6] 都已求出,通过上述过程知 n e x t [ 6 ] = 4 next[6]=4

A [ 7 ] = A [ 5 ] , n e x t [ 7 ] = n e x t [ 6 ] + 1 = 5 ∵A[7]=A[5],∴next[7]=next[6]+1=5

接下来考虑next[8]

发现 A [ 8 ] = { a } A[8]=\{a\} A [ 6 ] = { b } A[6]=\{b\} ,所以 n e x t [ 8 ] next[8] 不等于 n e x t [ 7 ] + 1 next[7]+1

那么只好将匹配长度 j j 缩短

根据上面的结论我们知道 j j 好可以等于 3 3 5 5 ,尝试延伸到 A [ 8 ] A[8]

但是我们发现 A [ 8 ] A[8] A [ 4 ] A[4] A [ 2 ] A[2] 都不匹配,于是只能从头匹配, n e x t [ 8 ] = n e x t [ 1 ] + 1 = 1 next[8]=next[1]+1=1

那我们怎么让程序知道当我们发现 A [ 8 ] ! = A [ 6 ] A[8]!=A[6] 时该去匹配 A [ 4 ] A[4] A [ 2 ] A[2] 呢?

n e x t [ 7 ] = 5 next[7]=5 说明从 7 7 往前 5 5 个字符是与 A [ 1...5 ] A[1...5] 匹配的。那我们下一步要寻找的也就是 5 5 之前的 j j 个字符与 A [ 1... j ] A[1...j] 相匹配,那么 7 7 往前 j j 个字符是与 A [ 1... j ] A[1...j] 匹配的。这个 j j 的答案就是 n e x t [ 5 ] next[5] ,其实就是 n e x t [ n e x t [ 7 ] ] next[next[7]]

于是我们就可以通过这种方式快速的找到下一步 j j 要跳到哪里去。

之后演示一下这一段预处理 n e x t next 的过程

next[1] = 0 ; // next[1]=0很明显
for (int i = 2, j = 0; i <= n; i++) { // 求next[i]时next[1...i-1]肯定已经求得
	while (j && a[i] != a[j + 1]) j = next[j] ; // 不断尝试匹配长度为j是否可行,如果失败,则枚举next[j]是否可行; 如果都不行,则 next[i]=0
	if (a[i] == a[j + 1]) j++ ; // 如果能够扩展成功,则匹配的长度j加1。
	next[i] = j ; // next[i]即为j
}
2.对字符串A与B进行匹配。

求出数组 f f f [ i ] f[i] 表示 B B 中以 i i 为结尾的子串与 A A 的前缀能够匹配的最大长度。

大家有没有发现这个定义与 n e x t next 数组非常相似?对,他们连求法都基本一致!

给一下 f f 的求解代码:

for (int i  = 1, j = 0; i <= m; i++) {
	while (j && (j == n || b[i] != a[j + 1])) j = next[j] ;
	if (b[i] == a[j + 1]) j++ ;
	f[i] = j ;
	if (f[i] == n) ans++ ; // 能够匹配的长度为n,表示匹配到一次,答案次数++
}

这就是 K M P KMP 的大体思路,时间复杂度 O ( n + m ) O(n+m)

三.例题选讲

首先是一道模板题:【模板】KMP字符串匹配

对于每个 f [ i ] = n f[i]=n 的点,其左端点就是 i n + 1 i-n+1 ,于是输出一下即可

代码

之后是一个裸题:[USACO15FEB]Censoring (Silver)

用一个栈去维护,如果栈中后缀与模式串匹配,那么就把栈后缀退栈

匹配的话可以用 K M P KMP ,当时脑子一热就写了哈希了,现在就只剩下哈希代码了

哈希模数被卡了一次,不能用 31 31 要用 37 37

代码

速度很快最慢的才 50 m s 50ms

之后这个题目需要对 n e x t next 数组做一些深入了解:[BOI2009]Radio Transmission

我们发现这个题目不太好用哈希因为有一些删除的东西。

如果最后一位匹配的前缀长度小于等于串长的 1 / 2 1/2 , 易知匹配上的后缀之前的部分即为循环部分(多余部分舍去)

如果最后一位匹配的前缀长度大于串长的 1 / 2 1/2 , 由于匹配的前缀与后缀完全相等, 则前缀去掉公共部分的前缀可由反复迭代相等证得该串由此部分反复自我复制得到

即该前缀的非公共部分与后缀相等长度的前缀相等,又与前缀的公共部分的前缀相等,而已完成比较的前缀与后缀完全相等,以此类推

所以答案就是 n n e x t [ n ] n-next[n]

代码

还有一些题目,我会慢慢放的。

猜你喜欢

转载自blog.csdn.net/HQG_AC/article/details/85793342