字符串: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}\),矛盾。于是命题成立。
代码:
时间复杂度 \(O(n)\)。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; }
应用
回到上述的问题,如何判断 \(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\)。可以自己思考一下为什么。

浙公网安备 33010602011771号