Loading

KMP

概念

令 \(|s|\) 为 \(s\) 的长度。

  • 子串:字符串 \(s\) 中任意的连续的一段被称为 \(s\) 的子串,特别地,空串也是 \(s\) 的子串。

  • 前缀:字符串 \(s\) 中的任意一个从 \(1\) 开始的子串被称为 \(s\) 的前缀。

  • 后缀:字符串 \(s\) 中的任意一个末尾为 \(|s|\) 的子串被称为 \(s\) 的后缀。

  • border:既是 \(s\) 的前缀又是 \(s\) 的后缀的字符串就是 \(s\) 的一个 border。

  • 周期:令 \(p\) 为 \(s\) 的周期,那么对于 \(1 \le i \le |s| - p\),都有 \(s_i = s_{i + p}\)。

用 pre(s, k) 表示 \(s\) 的长度为 \(k\) 的前缀,suf(s, k) 表示 \(s\) 的长度为 \(k\) 的后缀。

border 与周期

当 \(s\) 的长度为 \(k\) 的前缀为 \(s\) 的 border 时,\(|s| - k\) 一定是 \(s\) 的周期。

border 的传递性

1.\(s\) 是 \(t\) 的 border,\(t\) 是 \(r\) 的 border,那么 \(s\) 是 \(r\) 的 border。

2.\(s\) 是 \(r\) 的 border,\(t \ (|t| > |s|)\) 是 \(r\) 的 border,那么 \(s\) 是 \(t\) 的 border。

所以,我们用 mb(s) 表示 \(s\) 的最长 border,那么,\(s\) 的所有 border 就分别为 mb(s),mb(mb(s)) ……。

KMP

单模匹配算法,在一个文本串 \(s\) 中查找一个模式串 \(t\),计算复杂度为 \(O(|s| + |t|)\),是此类算法可以达到的最优复杂度,空间复杂度也很优秀,为 \(O(|s|)\)。

令 \(|s| = n, |t| = m\)。

暴力

枚举 \(1 \le i \le n\),暴力往后找 \(m\) 个字符,判断是否与 \(t\) 相等,时间复杂度为 \(O(n \times m)\)。

正解

由于我们的暴力在每次碰到一个不一样的字符(也就是失配)时,就会重头开始配对,会浪费很多时间去重复判断已经判断过的字符,而 kmp 就可以很好的解决这个问题。

我们用两个指针 \(i, j\) 分别表示当前匹配到 \(s\) 中的第 \(i\) 个字符, \(t\) 中的第 \(j\) 个字符了。

我们先举个例子:

s : a b a b a b a d b a b
t : a b a b a c b

我们会在 \(i = j = 6\) 时失配,这个时候,暴力做法会回到 \(i = 2, j = 1\),但是,我们其实可以直接这样做:

s : a b a b a b a d b a b
t :     a b a b a c b

因为 aba 是 ababa 的最长 border,所以我们可以直接将 \(j\) 挪到 \(4\),这样就相当于前缀需要挪到原来的某个后缀上,又要使得这个前缀和后缀相等,自然就是挪到 border 后一个啦。

失配数组(最长 border 长度)

令当前要求 pre(s, i) 的最长 border 长度,用 \(nxt_i\) 表示。

那么令 pre(s, i - 1) 的所有 border 的长度分别为 \(k_1, k_2, k_3, \dots ,k_m \ (k_1 > k_2 > k_3 > \dots > k_m)\),那么,我们就是需要找到一个最大的 \(k_j\),使得 \(s_{k_j + 1} = s_i\)。

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 10;

string s1, s2;
int nxt[N];

void get_fail() {
  int k = 0;
  nxt[1] = 0;
  for (int i = 2; i < s2.size(); i++) {
    while (k && s2[i] != s2[k + 1]) {
      k = nxt[k];
    }
    k += (s2[i] == s2[k + 1]), nxt[i] = k;
  }
}

void KMP() {
  int n = s1.size() - 1, m = s2.size() - 1;
  for (int i = 1, j = 0; i <= n; i++) {
    while (j && s1[i] != s2[j + 1]) {
      j = nxt[j];
    }
    j += (s1[i] == s2[j + 1]);
    if (j == m) {
      cout << i - m + 1 << '\n', j = nxt[j];
    }
  }
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> s1 >> s2, s1 = ' ' + s1, s2 = ' ' + s2;
  get_fail(), KMP();
  for (int i = 1; i < s2.size(); i++) {
    cout << nxt[i] << ' ';
  }
  return 0;
}

失配树

失配树就是用 nxt 数组建成的树。

性质

在失配树上,\(u, v\) 的最近公共祖先就是 pre(s, u) 和 pre(s, v) 的最长公共 border。

\(u, v\) 的所有祖先都是 pre(s, u) 和 pre(s, v) 的公共 border。

洛谷 P5829 by xiehanrui0817

洛谷 P3435 by luqyou

SDFZ 1028 by chengning0909

CF25E by chengning0909

posted @ 2025-05-04 10:10  Yan719  阅读(74)  评论(0)    收藏  举报