KMP总结

目录

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

三、高效计算前缀函数(线性算法)

核心优化

  1. π i + 1 ≤ π i + 1 \pii+1 \le \pii + 1 πi+1≤πi+1:相邻前缀函数值至多增加 1 1 1
  2. 当 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:

  1. 新子串必然是以 c c c 结尾的后缀
  2. 将 s + c s + c s+c 反转 ,记反转串为 T T T
  3. 原字符串中以 c c c 结尾的新后缀 → \rightarrow → T T T 中的新前缀
  4. 计算 T T T 的前缀函数 π \pi π,得到最大值 π max ⁡ \pi_{\max} πmax
  5. 新增子串数 = ( ∣ s ∣ + 1 ) − π max ⁡ = (|s| + 1) - \pi_{\max} =(∣s∣+1)−πmax
  6. 更新 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
相关推荐
youngerwang39 分钟前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
KaMeidebaby1 小时前
卡梅德生物技术快报|纯化重组蛋白实操详解
人工智能·python·tcp/ip·算法·机器学习
手写码匠2 小时前
从零实现 Prompt 工程引擎:结构化提示、自动优化与多轮自省体系
人工智能·深度学习·算法·aigc
无限码力2 小时前
阿里算法岗 0530笔试真题 - 多约束条件下的元素匹配统计
算法·阿里笔试真题·阿里机试真题·阿里算法岗笔试
lqqjuly2 小时前
MLA — 多头潜在注意力深度解析
深度学习·神经网络·算法
吴可可1233 小时前
SolidWorks草图转三维DWG技巧
算法
redaijufeng3 小时前
C++雾中风景7:闭包
c++·算法·风景
小欣加油4 小时前
leetcode287寻找重复数
数据结构·c++·算法·leetcode
尽兴-4 小时前
2.1 向量基础:Embedding、余弦相似度、欧氏距离、向量检索
算法·embedding·欧氏距离·向量检索·余弦相似度
Black蜡笔小新5 小时前
自动化AI算法训练服务器DLTM训推一体工作站赋能多行业智能化升级
人工智能·算法·自动化