前述:
以下讨论字符串下标均从0开始
解决的问题:
一个文本串$S$(主串)和一个模式串$P$,求$P$在$S$中出现的位置,或者$P$在$S$中出现的次数,等等问题。
暴力做法:
复杂度是$O(n*m)$,$n$是$P$的长度,$m$是$S$的长度
1 |
|
讨论:
暴力做法,字符失配以后,模式串往后移动一步,重新开始匹配
S: ABCDABCDABDE
P: ABCDABD
此时在$i=6$且$j=6$处失配($i$表示指向$S$的下标,$j$表示指向$P$的下标),由于失配,按照暴力做法,下一步的比较是下面这样的:
S: ABCDABCDABDE
P: ABCDABD
此时$i=1$,$j=0$重新开始比较
KMP的做法:发生不匹配的时候,$i$不移动,直接移动模式串的位置,也就是$j$的位置,假设新的$j$的位置是$k(k < j)$,那么要求尽可能大的$k$使得:
$S[i-k…i-1]=P[0…k-1]$(式子1)
失配的前提条件是:
$S[i-j…i-1] = P[0…j-1]$,并且$S[i] != P[j]$(式子2),
由于$k < j$,由式子2可得:
$S[i-k…i-1] = P[j-k…j-1]$(式子3)
那么由式子1和式子3可得$P[0…k-1]=P[j-k…j-1]$
下面用文字的方式来解释:
式子2表示$P$的长度是$j$的前缀等于$S$的长度是$i$的前缀的长度是$j$的后缀,然后此时$S[i] != P[j]$,也就是失配了,我们知道了前面相等的匹配信息,所以不能浪费这些信息。
式子1表示我们不动下标$i$,只动下标$j$,然后使得$P$的长度是$k$的前缀等于$S$的长度是$i$的前缀的长度是$k$的后缀。然后$k < j$并且$k$越大越好。
由式子2我们推出式子3,也就是$P$的长度是$j$的前缀的长度是$k$的后缀等于$S$的长度是$i$的前缀的长度是$k$的后缀。
根据上面的叙述,我们定义一个数组$next$:
$next[j] = k$,表示到$S[i] != P[j]$的时候,下一步$j$指针需要跳到$k$的位置
由上面的分析可以知道。$next[j]=k$表示为满足$P[0…k-1] = P[j-k…j-1]$的最大$k$
文字解释:
$next[j]$表示字符串$P$的长度是$j$的前缀的前缀和后缀相等的最大长度
P:ABCDABD
next:-1 0 0 0 0 1 2 0
如何求解next数组:
求解$next$数组的代码
1 | void kmp_pre(char P[], int m, int nxt[]){ |
当前我们已经知道$next[i] = j$,那么可知$P[0…j-1]=P[i-j…i-1]$,如果$P[i] == P[j]$,那么就是$P[0…j]=P[i-j…i]$,那么可以推知$next[i+1]=next[j+1]$,等价于$next[++i]=next[++j]$,如果$P[i] != P[j]$那么令$j=next[j]$,我们已经知道$P[0…next[j]-1]=P[i-next[j]…i-1]$,同样的道理,这样继续下去。可以看出$next$数组是字符串$P$自己和自己匹配的结果。
next数组的其他操作:
1.next数组的跳转:
沿着next数组一直往前,得到的是所有即使前缀也是后缀的字符串
next数组往前跳的步数的步长是一样的,除了最后一次
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
P | A | B | C | A | B | C | A | B | C | A | B | |
next | -1 | 0 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
从11开始跳:
得到11,8,5,2,0
从10开始跳:
得到10,7,4,1,0
从9开始跳:
得到9,6,3,0
从8开始跳:
得到8,5,2,0
等等。。。可以看出步长都是3
从11开始跳到8得到字符串AB
从8跳到5得到字符串ABCAB
从5跳到2得到字符串ABCABCAB
从2跳到0得到字符串ABCABCABCAB
以上四个字符串就是字符串$P$的既是前缀也是后缀的字符串
2.周期性字符串
如果$n \mod (n-next[n]) == 0$,那么循环节的长度是$n-next[n]$
推导一下:
对于模式串$P$有它的前缀$P[0…next[n]-1]$有$P[0…next[n]-1]=P[n-next[n]…n-1]$,通俗的说就是对于串$P[0…n-1]$,它的前半段长度是$k$的串和它的后半段长度是$k$的串相等。那么$n-next[n]$长的字符串就是后面的$P[n-(n-next[n])…n-1]$,也就是$P[next[n]…n-1]$,假设现在条件成立,即$n \mod (n-next[n]) == 0$,对于$next[next[n]]$,有$P[0…next[next[n]]-1]=P[next[n]-next[next[n]]…next[n]-1]$
综上可以推出:$P[next[next[n]]…next[n]-1] = P[next[n]…n-1]$
同样的道理推下去就可以知道循环节的长度是$n-next[n]$
举个例子:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
P | a | b | c | a | b | c | a | b | c | a | b | c | |
next | -1 | 0 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
条件:
条件1:
(abcabcabc) abc
abc (abcabcabc)
$P[0…next[n]-1]=P[n-next[n]…n-1]$
条件2:
(abcabc) abcabc
abc (abcabc) abc
$P[0…next[next[n]]-1]=P[next[n]-next[next[n]]…next[n]-1]$
推出结论:
(abcabc) [abc] abc
abc (abcabc) [abc]
$P[next[next[n]]…next[n]-1] = P[next[n]…n-1]$
相关题目练习:
1.POJ3461
题目描述:
给$t$组数据,每一组数据给两个字符串$W$和$T$,求字符串$W$在字符串$T$的出现次数
题解:
直接上kmp就完事了
1 |
|
2.hdu1711
题目描述:
有两个序列一个是长度为$n$的数字序列$a$,一个是长度是$m$的数字序列$b$,求最小的$k$使得$a[k…k+m-1] = b[1…m]$
题解:
就是求$b$在$a$中第一次出现的位置,直接上kmp就完事了
1 |
|
3.poj1961
题目描述:
求一个字符串的前缀中的周期性字符串个数
题解:
利用$next$数组的性质求解
1 |
|
4.poj2406
题目描述:
求一个字符串由几个周期性字符串构成
题解:
解法大致同上一题
1 |
|
5.hdu3336
题目描述:
求一个字符串中前缀在该字符串中出现的次数总和
题解:
解法1:
$dp[i]$ 表示长度是$i$的前缀的出现次数
$nxt[i]$表示前缀是$i$的字符串的前缀和后缀相等的最大长度
也就是说只要是长度为$i$的前缀在字符串中出现
容易知道$dp[i]$的初始值均为1
1 |
|
解法2:
$dp[i]$表示前$i$个字符中前缀的匹配次数,显然$dp[0]=0$
动态转移方程$dp[i]=dp[nxt[i]]+1$
也就是说:前$i$个字符中前缀的匹配次数等于前$nxt[i]$中前缀的匹配次数加上1,
加上的1显然就是长度为$i$的前缀
1 |
|
6.hdu3746
题目描述:
求最少在一个字符串后面条件多少个字符使得该字符串是一个周期性字符串
题解:
由next数组的周期性可以知道。令$rem = n - next[n]$,可以知道当前字符串被分成若干段长度是$rem$的字符串,但是最后一段长度不一定是$rem$,所以只要看模数是多少,补齐使得对$rem$取余是0就行了
1 |
|
7.hdu2594
题目描述:
求$s1$的前缀和$s2$的后缀的最大匹配
题解:
下面的做法是错误的,但是交上去还是对了,数据弱得一皮,过不了下面这一组数据
xxoooxxxxxxxxx
ooo
将$s1$和$s2$拼接成为$s$,求$s$的$nxt$数组。如果$nxt[n]==0$那么答案就是0这个显而易见。如果$nxt[n] <= min(|s1|, |s2|)$,那么答案长度就是$nxt[n]$,(错误分析开始)如果$nxt[n] > min(|s1|,|s2|)$,那么长度取$min(|s1|, |s2|)$
1 |
|
正确做法:
将$s1$和$s2$连接成$s$,求$s$的$next$数组,从$n$开始跳,直到跳到某个$j$使得$next[j] <= mn(|s1|, |s2|)$。这个其实用到了$next$数组的那个周期性的性质。每次跳的步数是$n-next[n]$,也就是说每次不要的字符串长度是$n-next[n]$这么长,最后得到的字符串长度$<=min(|s1|, |s2|)$而且相等。kmp的$next$数组简直就是米奇妙妙屋
1 |
|
8.poj2185
题目描述:
给一个字符串矩阵,求一个最小的矩阵使得把这个矩阵循环重复铺开之后它的一个子矩阵是给的那个矩阵
题解:
按行看,把每一行当一个字母,得到一个字符串,求这个字符串的$next$数组,则答案矩阵的$r = R - nxt[R]$,同样道理按列看,求得$c = C - nxt[C]$,答案为$r * c$。
1 |
|
9.hdu4763
题目描述:
在一个字符串$s$中求一个子串$t$,使得$t$在$s$的前,中,后位置都有匹配。
题解:
根据$next$数组的含义,$next[i]$表示字符串$s$的长度是$i$的前缀中的相等的最长前缀和后缀。所以答案就是$min(next[n], max(next[1…n-1]))$。
1 |
|
10.hdu6153
题目描述:
有两个字符串$s1$和$s2$,令$s2$的某一个后缀的权值是长度乘上该后缀在$s1$中的出现次数,求$s2$的所有后缀的权值和对$1e9+7$取模的值
题解:
首先想办法求$s2$中某一个后缀在$s1$中的出现次数。将$s1$和$s2$都逆序,问题就转换横求$s2$中的某一个前缀在$s1$中出现的次数。和hdu3336类似,hdu3336求的是
1 |
|