字符串复习笔记

1. KMP

A. 引子

KMP 一般用于进行字符串匹配相关的问题,我们称要拿去匹配的字符串 t 为模式串,被匹配的字符串 s 为文本串,需要直到 ts 中哪些位置出现。

第一想法是很直观的进行暴力,但是由某三位科学家做出了一些优化使得最终得到了一个 O(n) 的做法!

B. 详细过程

先对 t 求出一个前缀函数 nxti 表示对于前缀字符串 1i 来说,最长的 border 是多少。

一个字符串的 border 指的是一个前缀 1pos 满足前缀和后缀 npos+1n相等(注意,border 不能是它自己)

我们初始化 nxt1=0,因为一个字符串的 border 不能是它自己。

不妨假设我们求出了 nxt1i1 现在要考虑通过这些求出 nxti

image

现在要比较的其实就是蓝色箭头所指的位置 nxti1+1 和红色箭头所指的位置 i,如果 tnxti1+1=ti,那么 nxti=nxti1+1,这是最简单的情况。

但是如果 tnxti1+1ti,此时上次的前缀无法匹配了,我们只好去找一个更短的前缀尝试匹配,也就是 nxtnxti1

这一部分就是下图中前方被蓝线拦截的部分,因为它首先为前面绿色部分的 border,而绿色部分是相等的,于是它又是后面绿色部分的 border,于是是可以尝试匹配的。

image

一直这样匹配即可求出 nxt 数组。

nxt[1] = 0;
for (int i = 2; i <= m; i++) {
  int j = nxt[i - 1];
  while (j && t[i] != t[j + 1])  j = nxt[j];
  if (t[i] == t[j + 1])  j++;
  nxt[i] = j;
}

接下来需要考虑匹配的过程,我们匹配字符串 s,t 的时候也可以类似的做,我们记录 fi 表示字符串 1it 能匹配到 t 的第几位,然后如果可以匹配上就直接匹配,如果失配,就直接在 t 上暴力跳 nxt 数组即可。

for (int i = 1; i <= n; i++) {
  int j = f[i - 1];
  while (j && s[i] != t[j + 1])  j = nxt[j];
  if (s[i] == t[j + 1])  j++;
  f[i] = j;
}

下面证明它的复杂度为 O(n+m),只证明自我匹配的复杂度。

抽象问题为,一个容量为 m 升的水杯,初始为空,每次最多加一升水,加不超过 m 次,每次倒水最少倒一升,最多能倒几次?

nxt 的取值最大为 m,初始为 0,每次最多加一,加不超过 m 次,每次失配的时候最少减一,求最多跳多少次 nxt

不难发现是 O(m) 的,于是字符串匹配复杂度类似,总复杂度 O(n+m)

C. 扩展

1.KMP 自动机

KMP 自动机其实就是比 KMP 算法多做了一件事,它额外求出了一个数组 transi,j 表示在第 i 个位置上往后匹配一个字符 j 会转移到什么状态。

transi,j={i+1si+1=j0si+1ji=0transnxti,jsi+1ji0

前两种都很好理解,最后一种就是当无法匹配时,这个显然会等于 nxti 上一次匹配 c 的结果,于是直接 transi,j=transnxti,j,而且 transnxti,j 显然是已经之前求出来了的结果,因为 nxti<i

rep (i, 0, m) {
  for (int j = 0; j <= 9; j++) {
    if (i < m && s[i + 1] - '0' == j)  trans[i][j] = i + 1;
    else  if (!i)  trans[i][j] = 0;
    else  trans[i][j] = trans[nxt[i]][j];
    if (trans[i][j] < m)  base.mat[i][trans[i][j]]++;
  }
}

[HNOI2008]GT考试

求有多少个 n 位十进制数,满足其中不会出现一个 m 位的十进制数。

n109,m20


我们考虑一个 dpfi,j 表示当前为 n 位十进制数的第 i 位,和 m 位十进制数匹配到了第 j 位。

然后我们在额外求一个 gi,j 表示上次匹配长度为 i,在后面加入一个字符使得匹配长度为 j 的方案数为 gi,j

然后 g 求解就是一个 KMP 自动机就可以做完的事情。

fi,j=p=0m1fi1,p×gp,j

ans=i=0m1fn,i

发现 f 的转移很矩阵乘法,又因为 g 不变,然后就可以很舒服的矩阵快速幂,然后就做完了。

// 德丽莎你好可爱德丽莎你好可爱德丽莎你好可爱德丽莎你好可爱德丽莎你好可爱
// 德丽莎的可爱在于德丽莎很可爱,德丽莎为什么很可爱呢,这是因为德丽莎很可爱!
// 没有力量的理想是戏言,没有理想的力量是空虚
#include <bits/stdc++.h>
#define LL long long
using namespace std;
char ibuf[1 << 15], *p1, *p2;
#define getchar() (p1 == p2 && (p2 = (p1 = ibuf) + fread(ibuf, 1, 1 << 15, stdin), p1==p2) ? EOF : *p1++)
inline int read() {
  char ch = getchar();  int x = 0, f = 1;
  while (ch < '0' || ch > '9')  {  if (ch == '-')  f = -1;  ch = getchar();  }
  while (ch >= '0' && ch <= '9')  x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
  return x * f;
}
void print(LL x) {
  if (x > 9)  print(x / 10);
  putchar(x % 10 + '0');
}
template<class T> bool chkmin(T &a, T b) { return a > b ? (a = b, true) : false; }
template<class T> bool chkmax(T &a, T b) { return a < b ? (a = b, true) : false; }
#define rep(i, l, r) for (int i = (l); i <= (r); i++)
#define repd(i, l, r) for (int i = (l); i >= (r); i--)
#define REP(i, l, r)  for (int i = (l); i < (r); i++)
const int N = 30;
int n, m, mod, nxt[N];
char s[N];
int trans[N][27];
struct node {
  int mat[N][N];
  node () {  memset(mat, 0, sizeof(mat));  }
  node operator * (const node b) {
    node res;
    rep (i, 0, m - 1) 
      rep (k, 0, m - 1)
        rep (j, 0, m - 1)  
          (res.mat[i][j] += (mat[i][k] * b.mat[k][j] % mod)) %= mod;
    return res;
  }
} base, F;
node matpower(node a,int b) {
  node ans; 
  rep (i, 0, m - 1)  ans.mat[i][i] = 1;
  while (b) {  if (b & 1)  ans = ans * a;  a = a * a;  b >>= 1;  }
  return ans;
}
void solve() {
  cin >> n >> m >> mod;
  cin >> (s + 1);
  nxt[1] = 0;
  for (int i = 2; i <= m; i++) {
    int j = nxt[i - 1];
    while (j && s[j + 1] != s[i])  j = nxt[j];
    if (s[i] == s[j + 1])  j++;
    nxt[i] = j;
  }
  rep (i, 0, m - 1) {
    for (int j = 0; j <= 9; j++) {
      if (i < m && s[i + 1] - '0' == j)  trans[i][j] = i + 1;
      else  if (!i)  trans[i][j] = 0;
      else  trans[i][j] = trans[nxt[i]][j];
      if (trans[i][j] < m)  base.mat[i][trans[i][j]]++;
    }
  }
  F.mat[0][0] = 1;
  base = matpower(base, n);
  F = F * base;
  int ans = 0;
  rep (i, 0, m - 1)  ans += F.mat[0][i], ans %= mod;
  cout << ans ;
}
signed main () {
#ifdef LOCAL_DEFINE
  freopen("1.in", "r", stdin);
  freopen("1.ans", "w", stdout);
#endif
  ios :: sync_with_stdio(0);  cin.tie(0), cout.tie(0);
  int T = 1;  while (T--)  solve();
#ifdef LOCAL_DEFINE
  cerr << "Time elapsed: " << 1.0 * clock() / CLOCKS_PER_SEC << " s.\n";
#endif
  return 0;
}

2. border 论

周期:0<p<|s|,若 si=si+pi{1,2,,|s|p},那么 ps 的周期。

反过来,如果 ps 的周期,那么 pre(s,|s|p) 就为 s 的一个 border

pre(s,k): 表示 s1k

suf(s,k): 表示 s|s|k+1|s|

周期和 border 之间:
  • 36 都是 abcabcab 的周期。

  • abcabab 都是 abcabcabborder

  • pre(s,k)sborder |s|ks 的周期,如下图。

image

border 的传递性
  • stborder,串 trborder,那么 srborder。例子:abaababababorderababaababababorder,那么 abaabababorder

  • srborder,串 t|t|>|s|) 也是 rborder,那么 stborder

Fail 树

我们做了 KMP 之后的 nxtn 就是字符串 s 的最长 border

nxtn,nxtnxtn,s 的所有 border 集合。

s 的所有 border 环环相扣,被 1 条链串起来。

而如果我们将字符串中每一个位置 inxti 作为 i 的父亲连边。

那么最终得到了一个以 0 为根的树,这个数被称为 fail 树。

这样他会满足如下性质:

  • i 的所有祖先都是前缀 pre(s,i)border

  • s1is1j 的最长公共 border 为 Fail 树上两者的 lca(注意要特判一下 lcaij 的情况,需要再跳一次父亲,因为一个字符串的 border 不能是他自己)。

弱周期引理

p,qs 的周期,且 |p|+|q||s|,则 gcd(p,q) 也为 s 的周期。

证明:

p<q,记 d=pq

i<q 时, si=si+q=si+qp=si+d

i>p 时, si=sip=si+qp=si+d

因此 i 可以取遍 [1,|s|],所以 d 也是 s 的周期,根据辗转相除法,我们可以知道 gcd(p,d) 也为 s 的周期。

一个扩展是如果 |p|+|q|+gcd(|p|,|q|)|s|,那么 gcd(p,q) 也为 s 的周期,证明不太会.jpg

前缀整除周期传递性

st 的前缀,且 t 存在一个周期 as 存在一个周期 b,且 b|a|s|a,那么 T 也有周期 b

啊,还没写完,你先别急。

posted @   Pitiless0514  阅读(73)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示