KMP
1. 基础定义
在 KMP 算法中,通常约定字符串下标从 1 开始,这在逻辑处理上比 0 下标更方便。
- 子串:字符串 \(S\) 中从 \(i\) 到 \(j\) 这一段顺次排列形成的字符串,记作 \(S[i..j]\)。
- 子序列:从 \(S\) 中提取若干元素且不改变相对位置形成的序列。
- 真前缀:指除了 \(S\) 本身之外的所有前缀。
- 真后缀:指除了 \(S\) 本身之外的所有后缀。
- Border:若字符串 \(S\) 的一个真前缀等于其真后缀,则称该前缀为 \(S\) 的一个 Border。
2. 前缀函数 \(\pi[i]\)
2.1 定义
对于长度为 \(n\) 的字符串 \(s\),其前缀函数 \(\pi[i]\) 定义为:子串 \(s[1...i]\) 中最长的相等的真前缀与真后缀的长度。
- 若不存在相等的真前缀与真后缀,则 \(\pi[i] = 0\)。
- 特别规定 \(\pi[1] = 0\)。
- 数学描述:\(\pi[i] = \max_{k=1}^{i-1}\{k : s[1...k] = s[i-k+1...i]\}\)。
2.2 高效算法实现 (C++)
根据相邻前缀函数值至多增加 1 的性质,以及失配时通过 \(\pi\) 数组回溯的原理,可以得到 \(O(n)\) 的实现。
// s 以 1 下标开始,s[0] 可存放占位符
vector<int> prefix_function(string s) {
int n = (int)s.length() - 1;
vector<int> pi(n + 5);
// pi[1] 默认为 0
for (int i = 2; i <= n; i++) {
int j = pi[i - 1];
// 如果当前位置不匹配,跳转到上一个可能的 Border 长度
while (j > 0 && s[i] != s[j + 1]) j = pi[j];
// 如果匹配成功,Border 长度加 1
if (s[i] == s[j + 1]) j++;
pi[i] = j;
}
return pi;
}
3. KMP 算法的核心应用
3.1 字符串匹配
核心思想:构造新串 \(S' = S + \# + T\)。
- 分隔符 \(\#\) 必须是两串中均未出现过的字符,确保匹配长度不会跨越分隔符。
- 计算 \(S'\) 的前缀函数,若在文本串对应位置发现 \(\pi[i] = |S|\),则匹配成功。
- 起始位置还原:原文本串中的匹配起始下标为 \(i - 2 \cdot |S|\)。
3.2 字符串的周期与压缩
- 周期性质:若 \(r\) 是 \(s\) 的 Border 长度,则 \(|s|-r\) 是 \(s\) 的一个周期。
- 最小周期:根据最长 Border \(\pi[n]\),可得最小周期为 \(n - \pi[n]\)。
- 字符串压缩:定义 \(k = n - \pi[n]\)。若 \(k\) 能整除 \(n\),则最小压缩循环节长度为 \(k\);否则不可压缩,长度为 \(n\)。
3.3 统计前缀出现次数
- 统计每个前缀 \(s[1...i]\) 在自身中的出现次数:
利用前缀函数的嵌套性质(\(\pi[i], \pi[\pi[i]], \dots\) 均是其 Border),通过倒序累加实现:
vector<int> ans(n + 1, 1);
for (int i = n; i >= 1; i--) {
if (pi[i] > 0) ans[pi[i]] += ans[i];
}
4. 本质不同子串的数目
利用增量法实现 \(O(n^2)\) 求解:
- 每追加一个字符 \(c\) 时,新增的子串必然是以该字符结尾的后缀。
- 将当前串反转,任务转化为寻找“从未出现过的前缀”。
- 对反转串计算前缀函数,找出其中的最大值 \(\pi_{max}\)。
- 新增子串数 = 当前总长度 - \(\pi_{max}\)。

浙公网安备 33010602011771号