目录
- KMP总结
-
- 一、字符串基础定义
- [二、前缀函数( π \pi π 数组)](#二、前缀函数( π \pi π 数组))
- 三、高效计算前缀函数(线性算法)
- [四、KMP 字符串匹配算法](#四、KMP 字符串匹配算法)
- [五、周期与 Border](#五、周期与 Border)
- 六、统计前缀出现次数
- 七、动态维护本质不同子串数量
- 八、核心公式与结论汇总
KMP总结
一、字符串基础定义
- 子串:字符串中连续的一段
- 子序列:从字符串中按顺序取出若干字符(不要求连续)
- 后缀 :从某位置到字符串结尾的子串,记作 S u f f i x ( S , i ) = S i . . ∣ S ∣ Suffix(S,i) = Si..\|S\| Suffix(S,i)=Si..∣S∣
- 真后缀:不等于原串本身的后缀
- 前缀 :从开头到某位置的子串,记作 P r e f i x ( S , i ) = S 1.. i Prefix(S,i) = S1..i Prefix(S,i)=S1..i
- 真前缀:不等于原串本身的前缀
- 约定 :所有字符串下标从 1 1 1 开始
二、前缀函数( π \pi π 数组)
定义
对于长度为 n n n 的字符串 s s s, π i \pii πi 表示子串 s 1.. i s1..i s1..i 的最长相等真前缀与真后缀的长度 :
π i = max k = 1.. i { k : s 1.. k = s i − k + 1.. i } \pii = \max_{k = 1..i} \{k : s1..k = si-k+1..i\} πi=k=1..imax{k:s1..k=si−k+1..i}
- 若无相等,则 π i = 0 \pii = 0 πi=0
- 规定 π 1 = 0 \pi1 = 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 \pii+1 \le \pii + 1 πi+1≤πi+1:相邻前缀函数值至多增加 1 1 1
- 当 s i + 1 ≠ s π \[ i + 1 ] si+1 \ne s\\pi\[i+1] si+1=sπ\[i+1] 时,跳转到 j = π π \[ i ] j = \pi\\pi\[i] j=ππ\[i],继续尝试
最终代码
cpp
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 \pii = n πi=n( n n n 为模式串长度)时,找到匹配
- 匹配在文本串中的起始位置: i − 2 n i - 2n i−2n
方法二:直接匹配(常用)
cpp
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;
}
直接匹配写法
cpp
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 si = si+p si=si+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 ] ] , ... \pin, \pi\\pi\[n], \pi\\pi\[\\pi\[n]], \dots πn,ππ\[n],ππ\[π\[n]],...
- 最小周期 = n − π n = n - \pin =n−πn
字符串压缩
设 k = n − π n k = n - \pin 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 \pin πn 更长的 border,矛盾
- 证明中使用裴蜀定理: gcd ( k , p ) \gcd(k,p) gcd(k,p) 也是周期,且 gcd ( k , p ) < p \gcd(k,p) < p gcd(k,p)<p
六、统计前缀出现次数
在自身中出现
cpp
vector<int> ans(n + 1, 1);
for (int i = n; i >= 1; i--) ans[pi[i]] += ans[i];
- 最终 a n s i ansi ansi 表示前缀 s 1.. i s1..i s1..i 在 s s s 中出现的总次数
在另一个串中出现
- 构造 S ′ = s + "#" + t S' = s + \text{"\#"} + t S′=s+"#"+t
- 计算前缀函数,统计 π i = ∣ s ∣ \pii = |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 \pii = \max_{k = 1..i} \{k : s1..k = si-k+1..i\} πi=k=1..imax{k:s1..k=si−k+1..i} |
| 相邻关系 | π i + 1 ≤ π i + 1 \pii+1 \le \pii + 1 πi+1≤πi+1 |
| 失配跳转 | j = π j j = \pij j=πj |
| 最小周期 | n − π n n - \pin n−πn |
| 压缩条件 | ( n − π n ) ∣ n (n - \pin) \mid n (n−πn)∣n |
| 匹配位置(拼接法) | p o s = i − 2 n pos = i - 2n pos=i−2n |