KMP学习笔记

注意,本文的主要内容是《算法导论》书中的 KMP 算法,而非原始论文给出的形式。相比较于原始论文,算法导论中给出的 border 形式的 KMP 算法更加实用,原因是 XCPC 竞赛中 border 的作用远超过模式匹配,因此本文中的 next 数组定义为前缀子串最长的 border

如果你是考研选手,请查阅文末的原始论文定义!!!

Border

先定义两个符号:

  1. 字符串 s 长度为 i+1前缀子串记为 pre(s,i)
  2. 字符串 s 长度为 i+1后缀子串记为 suf(s,i)

例如字符串 s=ababacabpre(s,3)=aba,suf(s,4)=acab

Period

Period,即周期。在字符串 s 中,若满足 s[i]=s[i+p],i(0,1,,|s|p1) ,则 p 是字符串 s 的一个 period。

Border

Border,可以根据字面意思理解为边界(不知道确切译名)。在字符串 s 中,若满足 pre(s,p)=suf(s,p) ,则 p 是字符串 s 的一个 border,注意 p<|s| ,也就是说某个字符串的 border 不能是它本身。

因此,border 可以理解为:字符串 s 的所有公共前后缀(一个字符串可能有多个 border)。

Period 和 Border 的相互转化

一个推论:如果 p 是字符串 s 的一个 border,那么 |s|p 是一个 period。这个推论说明 period 和 border 某种意义上是等价的。

前缀函数

给定一个字符串 s,其前缀函数被定义为一个长度为 |s| 的数组 π
其中 π[i] 的定义是:

  1. 如果字符串 s 的前缀子串 pre(s,i) 有一对相等的真前缀与真后缀:pre(pre(s,i),k)suf(pre(s,i),k),那么 π[i] 就是这个相等的真前缀的长度,也就是 π[i]=k
  2. 如果不止有一对相等的,那么 π[i] 就是其中最长的那一对的长度;
  3. 如果没有相等的,那么 π[i]=0

因此,前缀函数 π[i] 实际上就是字符串 s 的前缀 pre(s,i)最长的 border

举例来说,对于字符串 abcabcd

  • π[0]=0,因为 a 没有真前缀和真后缀,根据规定为 0
  • π[1]=0,因为 ab 无相等的真前缀和真后缀。
  • π[2]=0,因为 abc 无相等的真前缀和真后缀。
  • π[3]=1,因为 abca 只有一对相等的真前缀和真后缀:a,长度为 1
  • π[4]=2,因为 abcab 相等的真前缀和真后缀只有 ab,长度为 2
  • π[5]=3,因为 abcabc 相等的真前缀和真后缀只有 abc,长度为 3
  • π[6]=0,因为 abcabcd 无相等的真前缀和真后缀。

因此字符串 abcabcd 的前缀函数为 [0,0,0,1,2,3,0]

前缀函数算法实现

在编程实现中,我们一般称前缀函数 π[]next[] 数组,其中 next[i] 表示前缀 pre(s,i) 的最长 border 长度。为了高效求出前缀函数,我们需要一些性质:

  • pre(s,i)pre(s,j) 的一个 border pre(s,i1)pre(s,j1) 的一个 border 且 s[i]=s[j]
  • border 具有传递性,假设长度为 n 的字符串 s 的所有 border 长度为 l1<l2<<lk ,那么可以推出:
    {next[n]=lknext[lk]=lk1next[lk1]=lk2
  • 根据 border 的传递性,可以推出一个迭代关系:pre(s,i) 的所有 border 可以表示为:next[i],next[next[i]],

根据上述性质,如果我们已知 next[0,1,,i1] ,想要求出 next[i] ,那就只需要从 j=next[i1] 开始,每次检查 s[j] 是否等于 s[i] ,如果该条件满足,则说明 next[i]=j ,否则令 j=next[next[i1]] …… 如此嵌套直到找到前缀函数。

模板

vector<int> getPrefixTable(string& s) {
int n = s.length();
vector<int> nxt(n, 0);
for (int i = 1; i < n; i++) {
int j = nxt[i - 1];
while (j > 0 && s[i] != s[j]) {
j = nxt[j];
}
if (s[i] == s[j]) j++;
nxt[i] = j;
}
return nxt;
}

时间复杂度分析:注意到 next[i1]+1next[i] ,因此回跳(j=next[j])的过程最多进行 O(n) 次,所以上述算法是 O(n) 的。

KMP 算法与字符串匹配

对长度为 n 的文本串 s 和长度为 m(mn) 的模式串 t 进行朴素的字符串匹配时,复杂度显然是 O(nm) 的。为了优化这个复杂度,我们可以在失配时利用前缀函数进行跳跃。

在字符串匹配过程中,我们用两个指针 i,j 分别指向文本串和模式串当前匹配到的位置,分两种情况考虑:

  1. 当遇到 s[i+1]=t[j+1] 时,说明可以继续匹配,因此两个指针继续往下移动:i++, j++
  2. 当遇到 s[i+1]t[j+1] 时,我们已知的信息是 suf(pre(s,i),j)=pre(t,j) ,也就是说他们有一段长度为 j+1 的公共前缀,此时两个指针需要跳转,而非朴素匹配中的归零。 此时,注意到这个公共前缀已经无法匹配,因此我们需要减小 j 指针,此时只需要 check 这个前缀子串的最长 border 能否匹配,如下:
  • 上图中演示了 s = ABABABABC, t = ABABC 进行匹配,匹配至 i=4 时失配(AC),此时由于已匹配的前缀串 ABAB 的最长 border 是 AB ,因此令 i=j=4i=4,j=2
  • 如果无法匹配则像求前缀函数时一样嵌套迭代,直到不存在 border 为止。当 j=m 时说明当前位置成功匹配,此时可以逆向算出匹配的位置为 i+1m ,然后令 j=next[m1] ,继续下一轮匹配。

最后,当 j=m 时说明当前位置成功匹配,此时可以逆向算出匹配的位置为 i+1m ,然后令 j=next[m1] ,继续下一轮匹配。

时间复杂度的分析非常简单,由于每一次匹配操作都会至少使得模式串右移 1 个单位,因此复杂度为 O(N+M)

实际上,最简单的理解就是:KMP 算法在失配之后,会直接跳跃到当前失配位置之前子串的最长 border 处。

模板

namespace KMP {
vector<int> getPrefixTable(string& s) { // 求前缀表
int n = s.length();
vector<int> nxt(n, 0);
for (int i = 1; i < n; i++) {
int j = nxt[i - 1];
while (j > 0 && s[i] != s[j]) {
j = nxt[j - 1];
}
if (s[i] == s[j]) j++;
nxt[i] = j;
}
return nxt;
}
vector<int> kmp(string& s, string& t) { // 返回所有匹配位置的集合
int n = s.length(), m = t.length();
vector<int> res;
vector<int> nxt = getPrefixTable(t);
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && j < m && s[i] != t[j]) {
j = nxt[j - 1];
}
if (s[i] == t[j]) j++;
if (j == m) {
res.push_back(i + 1 - m);
j = nxt[m - 1];
}
}
return res;
}
}

应用

字符串匹配

字符串匹配作为最经典的应用,已经在上文中写过了,可以直接去做模板题:P3375 【模板】KMP 字符串匹配

一些例题:

如果你想要学习字符串匹配问题的进阶,可以参考这篇文章:FFT 解决字符串匹配问题

Border & Period

在前面已经提到过:字符串中 Border 和 Period 是可以相互转化的,因此我们可以利用 border 解决很多周期相关的问题。

一些例题:

KMP 算法的两种不同定义

上方给出的 KMP 算法是《算法导论》中的定义,但是需要注意的是:该方法和原始论文的定义是不同的

原始论文中定义了两种数组 f[],next[],其中 f 数组的定义和上文中的前缀函数(next 数组)类似;但是原始论文中的 next[] 数组和现在我们常用的前缀函数有明显区别。

f 数组

原始论文中的 f 数组可以近似地看作前缀函数,其定义如下(下标从 1 开始,而不是常见的 0index):

f[j]={0j=1s1..j1border+1j1

例如字符串 abcabcacabf[] 数组就是:0,1,1,1,2,3,4,5,1,2,如下图:

不难发现,f 数组和前缀函数的定义基本上是一致的。

next 数组

注意,next[] 数组有时也被国内教材称为 KMP 算法的进一步优化,但是这个说法没什么道理。

原始论文中的 next[] 数组才是真正用来在失配时进行跳跃的,其定义就是:模式串在失配后应该跳到哪一位,如下图所示:

其中 next[j]=0 表示:当前位置 j 必然失配,我们应该把模式串的第 1 位滑动到文本串的第 j+1 位再进行匹配。

注意观察 next[] 数组和 f[] 数组的区别,第一个不同的位置是第 4 位,因此我们举例说明,比如文本串 sabcbabca,而模式串 tabca,显然当我们匹配到位置 j=4s4=bt4=a。此时,如果我们按照前文中的前缀函数方法进行匹配(其实和 f 数组一样),我们应该将模式串滑动到他的最长 border 处,这意味着:我们应该用 t1 对应 s4 ,然后比较 t1s4

但是,从上帝视角看,显然有 t1=t4,因此比较 t1s4 这一步是无意义的,实际上我们应该直接用 t1 去对应 s5。(观察 next[4]=0,也就是说明 j=4 这一位必定失配)出现这个无意义比较的原因就是存在 tj=tf[j],因此原始论文中的 next[] 数组的真实定义实际上是:前缀子串的最长 border,并且 tjtf[j],并且需要循环这一过程(令 f[j]=f[f[j]] )。

下面举例说明:例如 abcab,显然 f[5]=2 ,但是 t[5]=b=t[f[5]],因此我们继续检查 t[f[f[5]]] ,此时由于 t[f[f[5]]]=ab,因此 next[5]=1

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇