KMP总结
本文系统总结了KMP算法的核心内容:1)前缀函数(π数组)定义及线性计算方法;2)KMP字符串匹配的两种实现方式;3)周期与Border理论,包括最小周期计算和字符串压缩判定;4)前缀出现次数统计技术;5)动态维护本质不同子串数量的增量算法。全文以数学公式和代码实现相结合,涵盖了KMP算法的主要应用场景和优化技巧,关键结论包括π数组的单调性、失配跳转策略、最小周期公式(n-π[n])等,为字符串处
·
目录
KMP总结
一、字符串基础定义
- 子串:字符串中连续的一段
- 子序列:从字符串中按顺序取出若干字符(不要求连续)
- 后缀:从某位置到字符串结尾的子串,记作 S u f f i x ( S , i ) = S [ i . . ∣ S ∣ ] Suffix(S,i) = S[i..|S|] Suffix(S,i)=S[i..∣S∣]
- 真后缀:不等于原串本身的后缀
- 前缀:从开头到某位置的子串,记作 P r e f i x ( S , i ) = S [ 1.. i ] Prefix(S,i) = S[1..i] Prefix(S,i)=S[1..i]
- 真前缀:不等于原串本身的前缀
- 约定:所有字符串下标从 1 1 1 开始
二、前缀函数( π \pi π 数组)
定义
对于长度为 n n n 的字符串 s s s, π [ i ] \pi[i] π[i] 表示子串 s [ 1.. i ] s[1..i] s[1..i] 的最长相等真前缀与真后缀的长度:
π [ i ] = max k = 1.. i { k : s [ 1.. k ] = s [ i − k + 1.. i ] } \pi[i] = \max_{k = 1..i} \{k : s[1..k] = s[i-k+1..i]\} π[i]=k=1..imax{k:s[1..k]=s[i−k+1..i]}
- 若无相等,则 π [ i ] = 0 \pi[i] = 0 π[i]=0
- 规定 π [ 1 ] = 0 \pi[1] = 0 π[1]=0
示例
- s = "abcabcd" s = \text{"abcabcd"} s="abcabcd" → π = [ 0 , 0 , 0 , 1 , 2 , 3 , 0 ] \pi = [0,0,0,1,2,3,0] π=[0,0,0,1,2,3,0]
- s = "aabaaab" s = \text{"aabaaab"} s="aabaaab" → π = [ 0 , 0 , 1 , 0 , 1 , 2 , 2 , 3 ] \pi = [0,0,1,0,1,2,2,3] π=[0,0,1,0,1,2,2,3]
三、高效计算前缀函数(线性算法)
核心优化
- π [ i + 1 ] ≤ π [ i ] + 1 \pi[i+1] \le \pi[i] + 1 π[i+1]≤π[i]+1:相邻前缀函数值至多增加 1 1 1
- 当 s [ i + 1 ] ≠ s [ π [ i ] + 1 ] s[i+1] \ne s[\pi[i]+1] s[i+1]=s[π[i]+1] 时,跳转到 j = π [ π [ i ] ] j = \pi[\pi[i]] j=π[π[i]],继续尝试
最终代码
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n + 5);
for (int i = 2; i <= n; i++) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j + 1]) j = pi[j];
if (s[i] == s[j + 1]) j++;
pi[i] = j;
}
return pi;
}
- 时间复杂度: O ( n ) O(n) O(n)
四、KMP 字符串匹配算法
方法一:拼接法
- 构造字符串 S ′ = " " + s + "#" + t S' = \text{" "} + s + \text{"\#"} + t S′=" "+s+"#"+t
- 计算 S ′ S' S′ 的前缀函数
- 当 π [ i ] = n \pi[i] = n π[i]=n( n n n 为模式串长度)时,找到匹配
- 匹配在文本串中的起始位置: i − 2 n i - 2n i−2n
方法二:直接匹配(常用)
vector<int> find_occurrences(string text, string pattern) {
int n = pattern.size();
int m = text.size();
string cur = " " + pattern + "#" + text;
vector<int> pi = prefix_function(cur);
vector<int> ans;
for (int i = n + 2; i <= n + m + 1; i++) {
if (pi[i] == n) {
ans.push_back(i - 2 * n);
}
}
return ans;
}
直接匹配写法
vector<int> kmp(string t, string s) {
int n = s.length() - 1; // s 为 1-indexed
int m = t.length() - 1;
vector<int> pi = prefix_function(s);
vector<int> result;
int j = 0;
for (int i = 1; i <= m; i++) {
while (j > 0 && t[i] != s[j + 1]) j = pi[j];
if (t[i] == s[j + 1]) j++;
if (j == n) {
result.push_back(i - n);
j = pi[j];
}
}
return result;
}
- 时间复杂度: O ( n + m ) O(n + m) O(n+m)
五、周期与 Border
定义
- 周期:对 0 < p ≤ ∣ s ∣ 0 < p \le |s| 0<p≤∣s∣,若 s [ i ] = s [ i + p ] s[i] = s[i+p] s[i]=s[i+p] 对所有 i ∈ [ 1 , ∣ s ∣ − p ] i \in [1, |s|-p] i∈[1,∣s∣−p] 成立,则 p p p 是 s s s 的周期
- Border:若 s s s 长度为 r r r 的真前缀与真后缀相等,则称该前缀为 s s s 的 border
- 关系:存在长度为 r r r 的 border ⟺ \iff ⟺ ∣ s ∣ − r |s| - r ∣s∣−r 是周期
重要结论
- 所有 border 长度: π [ n ] , π [ π [ n ] ] , π [ π [ π [ n ] ] ] , … \pi[n], \pi[\pi[n]], \pi[\pi[\pi[n]]], \dots π[n],π[π[n]],π[π[π[n]]],…
- 最小周期 = n − π [ n ] = n - \pi[n] =n−π[n]
字符串压缩
设 k = n − π [ n ] k = n - \pi[n] k=n−π[n]:
- 若 k ∣ n k \mid n k∣n( k k k 整除 n n n),则最短压缩长度为 k k k
- 否则,无法压缩,答案为 n n n
正确性说明
- 若 k ∣ n k \mid n k∣n,字符串可划分为长度为 k k k 的若干块,所有块相等
- 若存在更小的压缩长度 p p p,则会产生比 π [ n ] \pi[n] π[n] 更长的 border,矛盾
- 证明中使用裴蜀定理: gcd ( k , p ) \gcd(k,p) gcd(k,p) 也是周期,且 gcd ( k , p ) < p \gcd(k,p) < p gcd(k,p)<p
六、统计前缀出现次数
在自身中出现
vector<int> ans(n + 1, 1);
for (int i = n; i >= 1; i--) ans[pi[i]] += ans[i];
- 最终 a n s [ i ] ans[i] ans[i] 表示前缀 s [ 1.. i ] s[1..i] s[1..i] 在 s s s 中出现的总次数
在另一个串中出现
- 构造 S ′ = s + "#" + t S' = s + \text{"\#"} + t S′=s+"#"+t
- 计算前缀函数,统计 π [ i ] = ∣ s ∣ \pi[i] = |s| π[i]=∣s∣ 的次数
七、动态维护本质不同子串数量
增量法思想
设当前字符串为 s s s,已有 k k k 个本质不同的子串,追加字符 c c c:
- 新子串必然是以 c c c 结尾的后缀
- 将 s + c s + c s+c 反转,记反转串为 T T T
- 原字符串中以 c c c 结尾的新后缀 → \rightarrow → T T T 中的新前缀
- 计算 T T T 的前缀函数 π \pi π,得到最大值 π max \pi_{\max} πmax
- 新增子串数 = ( ∣ s ∣ + 1 ) − π max = (|s| + 1) - \pi_{\max} =(∣s∣+1)−πmax
- 更新 k ← k + ( ( ∣ s ∣ + 1 ) − π max ) k \leftarrow k + ((|s| + 1) - \pi_{\max}) k←k+((∣s∣+1)−πmax)
算法特点
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)(每次追加需 O ( n ) O(n) O(n) 计算前缀函数)
- 可扩展到头部追加或两端删除
八、核心公式与结论汇总
| 内容 | 公式/结论 |
|---|---|
| 前缀函数定义 | π [ i ] = max k = 1.. i { k : s [ 1.. k ] = s [ i − k + 1.. i ] } \displaystyle \pi[i] = \max_{k = 1..i} \{k : s[1..k] = s[i-k+1..i]\} π[i]=k=1..imax{k:s[1..k]=s[i−k+1..i]} |
| 相邻关系 | π [ i + 1 ] ≤ π [ i ] + 1 \pi[i+1] \le \pi[i] + 1 π[i+1]≤π[i]+1 |
| 失配跳转 | j = π [ j ] j = \pi[j] j=π[j] |
| 最小周期 | n − π [ n ] n - \pi[n] n−π[n] |
| 压缩条件 | ( n − π [ n ] ) ∣ n (n - \pi[n]) \mid n (n−π[n])∣n |
| 匹配位置(拼接法) | p o s = i − 2 n pos = i - 2n pos=i−2n |
更多推荐




所有评论(0)