字符串:KMP 算法

引入

考虑这样一个题目,对于一个字符串 \(t\),求 \(s\) 在其中出现的次数。\(s,t\) 长度不超过 \(10^5\)。

显然一种朴素的做法是对于每个索引 \(i\) 开始在 \(t\) 上匹配 \(s\),这么做是 \(O(nm)\) 的,无法通过本题。

这样做很慢的原因是进行了很多重复匹配,比如 \(t\) 为 aacabacaab,\(s\) 为 aab。首先检查 \(i=1\),匹配到第 \(3\) 位,失败。其次检查 \(i=2,i=3\),我们发现这两次检查是没有必要的,KMP 算法就是优化了这些不必要的匹配。

KMP

前缀函数

对于一个长度为 \(n\) 的字符串 \(s\),定义前缀函数 \(f_i\) 表示 \(s_{0\dots i}\) 的最长公共真前后缀的长度,即 \(k=f_i\) 是满足一下条件的最大 \(k\) 值:\(s_{0\dots k-1}=s_{i-k+1\dots i}\),\(k\neq i\)。如果不存在则 \(f_{i}=0\)。

朴素求法

直接 \(O(n^3)\) 暴力,代码就不贴了。

优化

  • 性质 1:\(f_{i+1}\) 至多比 \(f_i\) 增加 1
    所以时间复杂度优化到 \(O(n^2)\)。
    这里从 OI-Wiki 上偷一段代码:
    vector<int> sol(const string &s){
      int n = (int)s.size();
      vector<int> f(n, 0);
      for(int i = 1; i < n; i++)
        for(int j = pi[i - 1] + 1; j >= 0; j--)
          if(s.substr(0, j) == s.substr(i - j + 1, j)){
            f[i] = j;
            break;
          }
      return f;
    }
    
  • 优化 2:当 \(i+1\) 失配时,如何跳转
    结论:当 \(s_{i+1}\neq s_{f_i}\) 时,仅次于 \(f_i\) 的第二长度为 \(f_{f_i-1}\)。
    求证:若 \(j\) 是 \(f_i\) 的最优候选项时,\(f_{j}+1\sim j-1\) 都不是最优取值。
    证明:假设存在 \(f_{j}<j'<j\),使得 \(j\) 为 \(f_i\) 的最优候选项。所以 \(s_{0\dots j'-1}=s_{i-j'\dots i-1}\)。容易推得 \(s_{j-j'\dots j-1}=s_{0\dots j'-1}\),矛盾。于是命题成立。
    代码:
    vector<int> get(string s){
        int n = s.size();
        vector<int> f(n, 0);
        for(int i = 1; i < n; i++){
            int j = f[i - 1];
            while(j > 0 && s[i] != s[j]) j = f[j - 1];
            if(s[i] == s[j]) j++;
            f[i] = j;
        }
        return f;
    }
    
    时间复杂度 \(O(n)\)。

应用

回到上述的问题,如何判断 \(s\) 在 \(t\) 中出现的次数,令 \(R=s+'\ '+t\),这里中间的空白字符也可以换成其它不在 \(s,t\) 字符集中的字符,对这个字符串求出前缀数组,满足 \(f_i=|s|\) 的个数就是答案。

例题:P3375

实现:

#include<bits/stdc++.h>
using namespace std;
vector<int> get(string s){
    int n = s.size();
    vector<int> f(n, 0);
    for(int i = 1; i < n; i++){
        int j = f[i - 1];
        while(j > 0 && s[i] != s[j]) j = f[j - 1];
        if(s[i] == s[j]) j++;
        f[i] = j;
    }
    return f;
}
signed main(){
    string s1, s2;
    cin >> s1 >> s2;
    vector<int> f = get(s2);
    string t = s2 + '#' + s1;
    vector<int> g = get(t);
    for(int i = s2.size() + 1; i < t.size(); i++){
         if(g[i] == s2.size()) cout << i - 2 * s2.size() + 1 << '\n';
    }
    for(int i : f) cout << i << ' ';
    cout << '\n';
    return 0;
}

例题

P4824

题目链接:https://www.luogu.com.cn/problem/P4824

用 KMP 匹配字符串,用一个栈记录回溯点以便于删除后指针指向下一个目标。

如果你会 AC 自动机,你可以尝试加强版。

UVA10298

题目链接:https://www.luogu.com.cn/problem/UVA10298

\(n-f_{n}\) 是字符串的最大周期的长度,如果它是 \(n\) 的因数,输出 \(n\times (n-f_{n})^{-1}\),否则答案就是 \(1\)。可以自己思考一下为什么。

posted @ 2026-10-04 20:39  地上飞的鱼  阅读(4)  评论(0)    收藏  举报