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

因为 abaababa 的最长 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  阅读(29)  评论(0)    收藏  举报