20260723 - KMP
可惜
std::find不是 \(O(n+m)\) 的算法啊。(注意不是 KMP)
暴力匹配
暴力匹配就是把模式串在文本串的每一个起始位置上,完整地比对一遍。
简单的来说,就是只要某个位置比对失败了,就把模式串整体往右移动一位,从头开始重新比对,直到走完为止(内层走完模式串算匹配成功,外层走完所有可能的起始位置算搜索结束)。
时间复杂度:\(O(n \times m)\)。
空间复杂度:\(O(1)\)(以下都记录算法复杂度)。
这个算法平均跑的很快,但是容易被卡。
std::find 可以在最坏 \(O(n \times m)\) 内进行字符串匹配,但平均 \(O(n+m)\)。
Hash 优化
说的没错,在暴力匹配的基础上,用 Hash 优化。
每次移动的代价是 \(O(1)\) 的,所以最后的复杂度是 \(O(n+m)\)。
但是注意 Hash 是可能被卡的,所以有更好的 KMP 算法。
PS:我们常说的字符串匹配 Hash 优化,其正式的算法名称是 Rabin-Karp 算法。
KMP 算法
KMP 能在线性时间复杂度进行单模式串匹配。
首先有一个 \(next\) 函数,先别管怎么求解的,他表示一个串的最长公共前后缀(border)。
如果能求出一个串的 border,就能把两个串拼起来来匹配了。
就比如 \(S = \text{ababababababcab}\),\(S = \text{abababc}\),可以先拼成 \(F = \text{abababc\$ababababababcab}\),然后再对 \(F\) 求 border。
求 border
情况一:匹配成功
若 \(F_{i+1} = F_{j+1}\)(且 \(j < m\)),则匹配长度增加 \(1\),此时文本指针前进一位。
情况二:匹配失败
若 \(F_{i+1} \ne F_{j+1}\),则当前字符无法扩展匹配。此时:
- 文本指针 \(i\) 保持不动,不回溯;
- 令 \(j = \text{next}_j\),将 \(j\) 回退到次长的可匹配前缀长度;
- 重复比较同一个 \(F_{i+1}\) 与新的 \(F_{j+1}\),直到 \(j = 0\) 或匹配成功为止。
注意:令 \(j = \text{next}_j\) 的时候,可能不止跳转一次。
我们发现,如果这样,第一个指针就不用回退了。
所以,时间复杂度是 \(O(n+m)\)。
代码:
vector<int> next_algorithm(const string &b) {
const string &s = '_' + b;
int len = (int)b.size();
vector<int> nxt(len + 1);
nxt[1] = 0; int j = 0;
for (int i = 2; i <= len; i++) {
while (j && s[i] != s[j + 1]) j = nxt[j];
if (s[i] == s[j + 1]) ++j;
nxt[i] = j;
}
return nxt;
}
vector<int> kmp(const string &a, const string &b) {
const string &s = b + '#' + a;
return next_algorithm(s);
}
例题
前言:exKMP 是个好东西,久违的用上
std::cin了好吧。
B - Radio Transmission 无线传输
发现答案是 \(n - \text{next}_n\)。
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n;
string a;
cin >> n >> a;
const auto &nxt = next_algorithm(a);
printf("%d\n", (int)a.size() - nxt.back());
return 0;
}
C - MUH and Cube Walls
在差分数组上做 KMP。
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &x[i]);
for (int i = 1; i <= m; i++)
scanf("%d", &y[i]);
if (m == 1) return printf("%d\n", n), 0;
for (int i = 1; i < n; i++)
a[i] = x[i + 1] - x[i];
for (int i = 1; i < m; i++)
b[i] = y[i + 1] - y[i];
for (int i = 1; i <= n; i++) debug(a[i]);
for (int i = 1; i <= m; i++) debug(b[i]);
kmp();
return 0;
}
D - Compress Words
找重叠即可,注意只要取后缀。
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n;
string pre, s;
cin >> n;
string ans;
for(int i = 1;i <= n;i++){
cin >> s;
if (i == 1) ans = s;
else {
int l1 = (int)ans.size();
int l2 = (int)s.size();
const auto &nxt = kmp(ans.substr(l1 - min(l1,l2)), s);
ans += s.substr(nxt.back());
}
}
cout << ans << '\n';
return 0;
}
E - OKR-Periods of Words
找到最小的然后减去就变成最大的了。
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n;
string s;
cin >> n >> s;
const auto &nxt = next_algorithm(s);
vector<int> ne = nxt;
s = '_' + s;
ll ans = 0;
for (int i = 2; i <= n; i++) {
int j = i;
while (ne[j]) j = ne[j];
if (nxt[i])
ne[i] = j;
ans += i - j;
}
printf("%lld\n", ans);
return 0;
}
F - Password
讲讲赛时做法吧。
首先求一遍 kmp 和$exKMP。
找到最大的,然后再跑一边 KMP。
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
string s;
cin >> s;
const auto &z = z_algorithm(s);
const auto &nxt = next_algorithm(s);
int maxx = -1, id = -1919810, n = (int)s.size();
for (int i = 1; i <= n; i++) {
if (z[i] > maxx && i + z[i] - 1 <= n) {
maxx = z[i];
id = i;
}
}
if (maxx == -1) return puts("Just a legend"), 0;
const string &t = s.substr(id - 1);
const auto &kmp = next_algorithm(t);
int len = (int)t.size();
int ans = min(nxt[n], kmp[len]);
if (!ans) puts("Just a legend");
else for (int i = 0; i < ans; i++) printf("%c", t[i]);
return 0;
}
KMP
把上面的 exKMP 替换成 KMP 就好了。
exkmp
判断长度就好了。
G - Prefixes and Suffixes
exKMP 神力。
首先求出 Z 函数,然后统计,差分求解即可。
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
string s;
cin >> s;
const auto &z = z_algorithm(s);
int n = (int)s.size();
for (int i = 1; i <= n; i++) {
++cnt[1];
--cnt[z[i] + 1];
}
for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
vector<PII> ans;
for (int i = 1; i <= n; i++) {
if (z[n - i + 1] == i) {
ans.push_back({i, cnt[i]});
}
}
printf("%d\n", (int)ans.size());
for (auto [x, y] : ans) printf("%d %d\n", x, y);
return 0;
}
PS:唯一一道题要人机验证的,痛失首 A(卡了20 min)。
H - Test
当然要枚举几种情况。
经过面向数据调试,终于过了。
就是可能有包含的情况。
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
string s[3];
cin >> s[0] >> s[1] >> s[2];
array<int, 3> arr{0, 1, 2};
int ans = inf;
do {
auto solve = [&]() {
string res;
int nk = 0;
for (int i = 0; i < 3; i++) {
if (i == 0) res = s[arr[i]];
else {
int l1 = (int)res.size();
int l2 = (int)s[arr[i]].size();
const auto &nxt = kmp(res.substr(l1 - min(l1,l2)), s[arr[i]]);
const string &now = s[arr[i]].substr(nxt.back());
res += now;
}
}
int n = (int)s[arr[0]].size(), m = (int)s[arr[1]].size(), ok = false;
const auto &nxt = kmp(s[arr[0]], s[arr[1]]);
for (int i = m + 2; i <= n + m + 1; i++) {
if (nxt[i] == m) ok = true;
}
if (ok) {
res = "";
for (int i = 0; i < 3; i++) {
if (i == 1) continue;
if (i == 0) res = s[arr[i]];
else {
int l1 = (int)res.size();
int l2 = (int)s[arr[i]].size();
const auto &nxt1 = kmp(res.substr(l1 - min(l1,l2)), s[arr[i]]);
const string &now = s[arr[i]].substr(nxt1.back());
res += now;
}
}
return res.size();
}
return res.size();
};
ans = min<int>(ans, solve());
} while (next_permutation(all(arr)));
printf("%d\n", ans);
// string l = "wlwobhnmmgtfolfaeckufuzdlsjxmevrtessfbwlnzzclcqwevnnucxyvhngnxhcbdfwqbrnnglylydxtgtvrlmeeszoiuatzzzx";
// printf("%d\n", (int)l.size());
// string l = "brnnglylydxtgtvrlmeeszoiuatzzzx";
// printf("%d\n", (int)l.size());
return 0;
}
// wlwobhnmmgtfolfaeckufuzdlsjxmevrtessfbwlnzzclcqwevnnucxyvhngnxhcbdfwqbrnnglylydxtgtvrlmeeszoiuatzzzx
I - 动物园
论看题的重要性。
KMP,然后往前跳,然后没了?
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int oT_To = 1;
cin >> oT_To;
while (oT_To--) {
string s;
cin >> s;
int n = (int)s.size();
const auto &nxt = next_algorithm(s);
ll ans = 1;
s = '_' + s;
int j = 0;
for (int i = 2; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = nxt[j];
if (s[i] == s[j + 1]) ++j;
while (j >= i - j + 1) j = nxt[j];
ans = (ans * (p[j] + 1)) % P;
}
printf("%lld\n", ans);
}
return 0;
}
J - Om Nom and Necklace
KMP 太困难了,让我们用 exKMP 秒掉他。
大概思路:将原序列分成 \(k\) 个块,每个块中的每一个元素可以用 \(z\) 函数判断(具体见代码注释),然后用差分维护。
代码:
vector<int> z_algorithm(const string &a) { // 求 Z 函数,不会的可以看看模板。
const string &c = '_' + a;
int n = (int)c.size() - 1;
vector<int> z(n + 1);
int L = 1, R = 0;
z[1] = n;
for (int i = 2; i <= n; i++) {
if (i > R) z[i] = 0;
else {
int k = i - L + 1;
z[i] = min(z[k], R - i + 1);
}
while (i + z[i] <= n && c[z[i] + 1] == c[i + z[i]]) ++z[i];
if (i + z[i] - 1 > R) {
R = i + z[i] - 1;
L = i;
}
}
return z;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n, k;
string s;
cin >> n >> k >> s;
const auto &z = z_algorithm(s);
s = '_' + s;
vector<int> d(n + 10);
for (int len = 1; len <= n / k; len++) { // 最多有 n / k 个块。
bool ok = true;
for (int i = 1; i < k; i++) { // 枚举块中的元素,如果可以匹配的都已经小于 len,就一定不行。
if (len * i + 1 <= n && z[len * i + 1] < len) {
ok = false;
break;
}
}
if (ok) { // 如果可以的话,可以凑出来的起点是 len * k,终点就是分的块的长度和下一个能匹配上的最小值。
++d[len * k];
--d[len * k + min(len, z[len * k + 1]) + 1];
}
}
for (int i = 1; i <= n; i++) d[i] += d[i - 1];
for (int i = 1; i <= n; i++) printf("%d", d[i] > 0); // 这里有可能不是 1,所以得判断 d[i] > 0。
return 0;
}
后记
exKMP 得了 MVP。

浙公网安备 33010602011771号