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[ik+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]

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

核心优化

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

方法二:直接匹配(常用)

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<ps,若 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,sp] 成立,则 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 sr 是周期

重要结论

  • 所有 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 kn k k k 整除 n n n),则最短压缩长度为 k k k
  • 否则,无法压缩,答案为 n n n

正确性说明

  • k ∣ n k \mid n kn,字符串可划分为长度为 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

  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}) kk+((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[ik+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=i2n
Logo

一站式 AI 云服务平台

更多推荐