字符串匹配有很多方法,比如暴力,哈希等等,还有一种广为人知的算法
−
−
−
K
M
P
---KMP
−−−KMP。
一.问题引入
需要一种算法,能够在线性的时间内判断 a [ 1... N ] a[1...N] a[1...N]是否是字符串 b [ 1... M ] b[1...M] b[1...M]的字串,并要求返回字符串a在b中匹配的所有位置
思考暴力。
枚举i从 1 − > m 1->m 1−>m表示 b b b匹配的左端点,然后 O ( n ) O(n) O(n)的判断 b [ i . . . i + n − 1 ] b[i...i+n-1] b[i...i+n−1]是否与 a [ 1... n ] a[1...n] a[1...n]匹配。
我们很容易发现这样的方法在极端数据下跑的很慢,比如:
aaaaaaaaaaaaaaaaaab
aaaaab
每次要匹配到a最后一个位置才发现不相等。时间复杂度 O ( n m ) O(nm) O(nm)
当然此问题亦可以通过哈希解决,笔者在此不多赘述
接下来我们就讲神奇的
K
M
P
KMP
KMP算法
二.算法概述
K M P KMP KMP算法,又称模式匹配算法,是一种能够高效,准确的处理字符串匹配的算法
KMP算法基本分为两部:
1.首先是对 A A A数组(模式串)进行自我匹配。
建立一个 n e x t next next数组, n e x t [ i ] next[i] next[i]表示以 i i i结尾的非前缀字串与A的前缀能够匹配的最大长度。
其中“以 i i i结尾的非前缀字串”通俗的说就是非前缀的后缀,比如aab的非前缀的后缀就是 { b } , { a b } \{b\},\{ab\} {b},{ab}
n e x t [ i ] = max { j } , j < i next[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[i−j+1...i]=A[1...j]
举个例子:
设 A A A串为 " a b a b a b a a c " "abababaac" "abababaac",A数组的next[7]应该为5,推导过程如下:
发现有三个可行的j满足 A [ i − j + 1... i ] = A [ 1... 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[7...7]={a}与
A
[
1...1
]
=
{
a
}
A[1...1]=\{a\}
A[1...1]={a} 匹配;
A
[
5...7
]
=
{
a
b
a
}
A[5...7]=\{aba\}
A[5...7]={aba}与
A
[
1...3
]
=
{
a
}
A[1...3]=\{a\}
A[1...3]={a}匹配;
A
[
3...7
]
=
{
a
b
a
b
a
}
A[3...7]=\{ababa\}
A[3...7]={ababa}与
A
[
1..5
]
=
{
a
b
a
b
a
}
A[1..5]=\{ababa\}
A[1..5]={ababa}匹配;
其中 j j j最大的是第 3 3 3个为 5 5 5
如何更快的计算 n e x t next next数组?
不妨设 n e x t [ 1...6 ] next[1...6] next[1...6]都已求出,通过上述过程知 n e x t [ 6 ] = 4 next[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 ∵A[7]=A[5],∴next[7]=next[6]+1=5
接下来考虑next[8]
发现 A [ 8 ] = { a } A[8]=\{a\} A[8]={a}而 A [ 6 ] = { b } A[6]=\{b\} A[6]={b},所以 n e x t [ 8 ] next[8] next[8]不等于 n e x t [ 7 ] + 1 next[7]+1 next[7]+1
那么只好将匹配长度 j j j缩短
根据上面的结论我们知道 j j j好可以等于 3 3 3和 5 5 5,尝试延伸到 A [ 8 ] A[8] A[8]
但是我们发现 A [ 8 ] A[8] A[8]与 A [ 4 ] A[4] A[4]和 A [ 2 ] A[2] A[2]都不匹配,于是只能从头匹配, n e x t [ 8 ] = n e x t [ 1 ] + 1 = 1 next[8]=next[1]+1=1 next[8]=next[1]+1=1
那我们怎么让程序知道当我们发现 A [ 8 ] ! = A [ 6 ] A[8]!=A[6] A[8]!=A[6]时该去匹配 A [ 4 ] A[4] A[4]和 A [ 2 ] A[2] A[2]呢?
n e x t [ 7 ] = 5 next[7]=5 next[7]=5说明从 7 7 7往前 5 5 5个字符是与 A [ 1...5 ] A[1...5] A[1...5]匹配的。那我们下一步要寻找的也就是 5 5 5之前的 j j j个字符与 A [ 1... j ] A[1...j] A[1...j]相匹配,那么 7 7 7往前 j j j个字符是与 A [ 1... j ] A[1...j] A[1...j]匹配的。这个 j j j的答案就是 n e x t [ 5 ] next[5] next[5],其实就是 n e x t [ n e x t [ 7 ] ] next[next[7]] next[next[7]]
于是我们就可以通过这种方式快速的找到下一步 j j j要跳到哪里去。
之后演示一下这一段预处理 n e x t next 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, f [ i ] f[i] f[i]表示 B B B中以 i i i为结尾的子串与 A A A的前缀能够匹配的最大长度。
大家有没有发现这个定义与 n e x t next next数组非常相似?对,他们连求法都基本一致!
给一下 f 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 KMP的大体思路,时间复杂度 O ( n + m ) O(n+m) O(n+m)
UPD: 还有,如果你动动笔画个图,你会发现 n e x t next next数组构成了一棵树,这样 n e x t next next数组也被叫做 n e x t next next树。
三.例题选讲
首先是一道模板题:【模板】KMP字符串匹配
对于每个 f [ i ] = n f[i]=n f[i]=n的点,其左端点就是 i − n + 1 i-n+1 i−n+1,于是输出一下即可
之后是一个裸题:[USACO15FEB]Censoring (Silver)
用一个栈去维护,如果栈中后缀与模式串匹配,那么就把栈后缀退栈
匹配的话可以用 K M P KMP KMP,当时脑子一热就写了哈希了,现在就只剩下哈希代码了
哈希模数被卡了一次,不能用 31 31 31要用 37 37 37
速度很快最慢的才 50 m s 50ms 50ms
之后这个题目需要对 n e x t next next数组做一些深入了解:[BOI2009]Radio Transmission
我们发现这个题目不太好用哈希因为有一些删除的东西。
如果最后一位匹配的前缀长度小于等于串长的 1 / 2 1/2 1/2, 易知匹配上的后缀之前的部分即为循环部分(多余部分舍去)
如果最后一位匹配的前缀长度大于串长的 1 / 2 1/2 1/2, 由于匹配的前缀与后缀完全相等, 则前缀去掉公共部分的前缀可由反复迭代相等证得该串由此部分反复自我复制得到
即该前缀的非公共部分与后缀相等长度的前缀相等,又与前缀的公共部分的前缀相等,而已完成比较的前缀与后缀完全相等,以此类推
所以答案就是 n − n e x t [ n ] n-next[n] n−next[n]
UPD:
蓝书上的题目Period
此题显然可以使用二分+哈希
通过上一题,我们知道一个长度为n的串的最小循环节长度为 n − n e x t [ n ] n-next[n] n−next[n],这一题中这个结论也同样适用
假设 S [ 1... i ] S[1...i] S[1...i]有长度为 x x x的循环节,显然 x x x 必定整除 i i i ,且 S [ 1... i − l e n ] S[1...i-len] S[1...i−len] 与 S [ l e n + 1... i ] S[len+1...i] S[len+1...i] 也相等(因为循环么)
根据上一题的结论,我们知道 x x x 就等于 n − n e x t [ n ] n-next[n] n−next[n] ,并且循环次数=长度/循环节长度
于是就做完了
依然是有关于最小循环节长度相关的问题
我们不怕删除,Radio Transmission 这题已经告诉了我们这个结论
把每一行当成一个字符跑一边最小循环节,同理列也跑一遍,这样就求出来了循环面积的尺寸,把他们乘起来就是答案
这是个很棒的题目,他诠释了另一种 n e x t next next 数组的应用,就是 n e x t next next 树
很明显我们现在要解决的问题就是求出一个 s u m sum sum 数组表示前缀s[1…i]在串中出现了多少次,再乘以长度就可以算出答案了
拿这个东西怎么搞?
首先,如果你真的理解了 n e x t next next 的意义,你会很容易的发现:
如果一个位置 j j j 他的 n e x t [ j ] = i next[j]=i next[j]=i ,那么他 S [ 1.. j ] S[1..j] S[1..j] 中肯定出现了 S [ 1... i ] S[1...i] S[1...i]
于是我们发现 s u m [ i ] = ( ∑ n e x t [ j ] = i s u m [ j ] ) + 1 sum[i]=(\sum\limits_{next[j]=i}sum[j])+1 sum[i]=(next[j]=i∑sum[j])+1
因为可以肯定 n e x t [ i ] < = i next[i]<=i next[i]<=i ,所以我们就也以倒着枚举算出答案了
然后你画个图发现 n e x t next next 数组构成了一棵树, n e x t next next 树上的一个节点是他所有子树的前缀,而 s u m sum sum 数组存储的其实是他子树的大小
暴力统计 n u m num num 数组一直向前跑,肯定会T
怎么优化?
我们发现,这个题目的关键是前后两个字符串(前缀与后缀)不可以重合
那么我们不妨先忽略掉这一个条件,求出有交叉的 a n s ans ans 数组,那么显而易见 n u m num num 数组 就是 n u m [ i ] = a n s [ j ] num[i]=ans[j] num[i]=ans[j]( j j j 是小于 i i i 的一半的那个数,在 n e x t next next 上跳)
那么本题的重点便到了求出 a n s ans ans 数组了,然而显而易见的只要在求 K M P KMP KMP 的时候顺便推出便可以了
然后由于 n u m num num 数组用了就扔,所以就根本不用求出来!!
啊哈一道NOI题就这样被切了? 感觉不错。
之后我还需要做一道题:GT考试,听lls说是矩乘+KMP