KMP算法是一种改进的[字符串匹配]算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为KMP算法。时间复杂度O(m+n)。
本文适合那些知道一点KMP,但是看了很多博客都有点懵的同学。阅读之前,我强烈推荐先阅读这篇文章:如何更好的理解和掌握KMP算法
本文其实也只是对这篇文章的梳理和归纳。
我们的目的是在主串中匹配模式串。
KMP的关键是一个PMT数组(部分匹配表Partial Match Table)。假设模式串是abababca。那么对应的PMT是:
char | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
PMT | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
next | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
其中next只不过是PMT数组右移一位得到的。
这里,PMT的值含义是:字符串的前缀集合与后缀集合的交集中最长元素的长度。换个通俗的说法:后缀和前缀最长有多长是一样的。按表里的例子来说。ababa 这个串,最长的前后缀相同的串为前缀的{aba}和后缀的{aba},所以对应值为3,再来个更浅显的例子。假设有个串是abcdefghijkabcd,那么,显然这个串的PMT值应该是4,即前缀的{abcd}和后缀的{abcd}相同,长度为4。
接下来是最关键的:
以知乎回答中的为例:
0 1 2 3 4 5 6 7 8 9
a b a b a b a b c a
a b a b a b c a
目前比较到了6号位置,主串和模式串的下标i和j都是6。
在6号位置,主串的a和模式串的c发生了失配,那么我们可以得到这样一个事实:失配前的字符,即0到5的字符都是不失配的。即主串中0到5的字符和模式串0到5的一样的。又因为我们有PMT表,知道模式串中对于0到5的ababab而言,前缀和后缀的相同字符数是4,即前缀的{abab}和后缀的{abab},那么也可以断定主串的后缀{abab}和模式串的前缀{abab}相同,于是,我们主串中的下标i别动,让j移动到PMT[j-1],就是PMT[6-1]=PMT[5]=4,继续进行比较即可。
为了每次跳转的时候不写作PMT[j-1],所以拎出来了next数组的概念,将PMT表右移一位,那么以后每次在j处失配,主串的i别动,模式串的j跳到next[j]继续进行比较即可。
其中要特地说明一下,j==-1的时候意味着第0位都失配了,所以j的位置要变成0(即j++),i的位置也要后移即i++,而这和未失配的处理情况是一样的,所以可以用一个||来统一。
在求取next的时候,其实就是模式串和自己的前缀做KMP字符串匹配,初始化i=0,j=-1是为了营造:第0位就失配的局面,这样匹配求出来的后缀,才是真后缀。
KMP算法代码有:
int kmp(char *t, int n, char *p, int m) {
int i = 0, j = 0;
while (i < n && j < m) {
if (j == -1 || t[i] == p[j]) ++i, ++j;
else j = next[j];
}
if (j == m) return i - j;
else return -1;
}
求取next数组:
void getNext(char *p, int m, int *next) {
next[0] = -1;
int i = 0, j = -1;
while (i < m) {
if (j == -1 || p[i] == p[j]) ++i, ++j, next[i] = j;
else j = next[j];
}
}
扩展KMP:
十分建议先读个几次这个ppt,然后会十脸懵c,如果懂了就直接return吧。
我总结了下我懵的原因:对定义不熟悉。熟悉之后很多操作都是很自然的事情。主要参考材料还是上述那个ppt。还是懵的话可以再瞅一眼这个视频,直接拉到22:00开始看。
一定得牢记定义:extend[i] 是S从i开始的后缀和T的最长公共前缀长度。而next[i] 是T从i开始的后缀和T的最长公共前缀长度。主串都是T,即都是和T相匹配的最长公共前缀长度,extend的模式串是S的后缀,next的模式串是T的后缀!
这段定义读多几次,记忆牢固之后再读ppt,会顺畅很多。假设你基本已经顺畅之后(懒),我进行一个梳理。
先来个感性的认识:假设S的后缀[i,i+1,..u] == T的[0,1,...u-i+1],那么S的[i+1,..u]必然等于T的[1,..u-i+1],我们如果又知道T的[1,..u-i+1]和T的最长公共前缀的话,那么就相当于知道了S的[i+1,..u]和T的最长公共前缀了!
当然,这之中还有一些细节要讨论的:
extend[i] 是S从i开始的后缀和T的最长公共前缀长度。next[i] 是T从i开始的后缀和T的最长公共前缀长度
如果extend[1..k]已经算好,而且在1到k这些位置中,和T能匹配到的最远位置是p,其中p=max(i+extend[i]-1),i=1,2,..k。假设是在a这个点取得max的,即p=a+extend[a]-1。因为extend[k]已经算好,所以S[a..p]=T[1..p-a+1]
所以必然S[k+1..p]=T[k-a+1..p-a+1],我们令L=next[k-a+1]。利用上面说的感性认识,这里其实就是相当于有两个串u(S[k+1..p])和v(T[k-a+1..p-a+1]),现在u=v,所以我们得到以v开头的串和T的最长公共前缀长度,即L,来加速我们的计算。
目的是算extend[k+1],情况分2种:
这两种情况我做一个比喻:有两根竹子a和b,a长为7段,b为10段。我现在要问你a竹子的颜色分布。
我和你说a的前5段和b的前5段对应段颜色一样。假如我和你说,b只有前3段颜色是黄色的,因为这个L=3 < 5,所以说,你必然可以断定a竹子的前3段是黄色的。这对应第一种情形。
假如我和你说,b的4到8段是绿色的,这个时候,你是不能断定a的下一个颜色分布的。你只能知道a的4到5段是绿色,那是否绿色仅仅延伸到5段呢?不可断定,于是你得带上眼镜凑上去从第6段开始去比较了。这对应第二种情形。
情形一:
如果k+L
什么意思呢?就是说u=v,以v开头的串和T的最长公共前缀L非常小,小到都在已探索的范围内,所以直接extend[a]=L。
情形二:
但是如果k+L>=p,意思就是说u=v,v开头的串和T的最大公共前缀串长度已经不止从k+1到p这段距离。 但是!我们现在只知道S[k+1,..p]=T[1,..p-k],就是这p-k个元素相等,而L提供的信息虽然多,但是对S[p+1]是否等于T[p-k+1]都不知道,那你的L对我而言有些信息是无效的。于是,我们得从S[p+1]和T[p-k+1]开始比较,直到失配,此刻失配的位置到k+1这个位置的长度才是extend[k+1]。留意,因为p位置必然后移(后移0位也算,统一一下),所以p和a的值都得更新一下。
void PRE_EKMP(char *x, int m, int *nxt) {
nxt[0] = m;
int j = 0;
while (j + 1 < m && x[j] == x[j + 1]) ++j;
nxt[1] = j;
int k = 1;
FOR(i, 2, m - 1) {
int p = nxt[k] + k - 1;
int L = nxt[i - k];
if (i + L - 1 < p) nxt[i] = L;
else {
j = max(0, p - i + 1);
while (i + j < m && x[i + j] == x[j]) ++j;
nxt[i] = j;
k = i;
}
}
}
void EKMP(char *x, int m, char *y, int n, int *nxt, int *extend) {
PRE_EKMP(x, m, nxt);
int j = 0;
while (j < n && j < m && x[j] == y[j]) ++j;
extend[0] = j;
int k = 0;
FOR(i, 1, n - 1) {
int p = extend[k] + k - 1;
int L = nxt[i - k];
if (i + L < p + 1) extend[i] = L;
else {
j = max(0, p - i + 1);
while (i + j < n && j < m && y[i + j] == x[j]) ++j;
extend[i] = j;
k = i;
}
}
}
当知悉了扩展KMP的加速思路之后,对Manacher算法的理解会非常有帮助,这个算法可以直接看Manacher算法详解
我大概总结一下,T[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如S[l,r]是回文串,那么T[i]=r-i+1。
要计算当前的T[i],即T[k](k 其余情况,比如T[j]==mx-i 或者根本i>mx,都需要老实一个个去对比。
模板如下:
char Ma[maxN*2], s[maxN];
int Mp[maxN*2];
void Manacher(char *s, int L) {
int l = 0;
Ma[l++] = '$';
Ma[l++] = '#';
FOR(i, 0, L - 1) {
Ma[l++] = s[i];
Ma[l++] = '#';
}
Ma[l] = 0;
int mx = 0, id = 0;
FOR(i, 0, l - 1) {
Mp[i] = mx > i ? min(Mp[2 * id - i], mx - i) : 1;
while (Ma[i + Mp[i]] == Ma[i - Mp[i]]) Mp[i]++;
if (i + Mp[i] > mx)
mx = i + Mp[i], id = i;
}
}
// 最后 max{Mp[i]-1} 就是答案。