字符串复习笔记
\(\mathcal{1.}\) KMP
\(\mathcal{A.}\) 引子
KMP 一般用于进行字符串匹配相关的问题,我们称要拿去匹配的字符串 \(t\) 为模式串,被匹配的字符串 \(s\) 为文本串,需要直到 \(t\) 在 \(s\) 中哪些位置出现。
第一想法是很直观的进行暴力,但是由某三位科学家做出了一些优化使得最终得到了一个 \(O(n)\) 的做法!
\(\mathcal{B.}\) 详细过程
先对 \(t\) 求出一个前缀函数 \(nxt_i\) 表示对于前缀字符串 \(1 \sim i\) 来说,最长的 \(border\) 是多少。
一个字符串的 border 指的是一个前缀 \(1\sim pos\) 满足前缀和后缀 \(n - pos + 1\sim n\)相等(注意,border 不能是它自己)
我们初始化 \(nxt_{1} = 0\),因为一个字符串的 \(border\) 不能是它自己。
不妨假设我们求出了 \(nxt_{1\sim i - 1}\) 现在要考虑通过这些求出 \(nxt_{i}\)。
现在要比较的其实就是蓝色箭头所指的位置 \(nxt_{i-1} + 1\) 和红色箭头所指的位置 \(i\),如果 \(t_{nxt_{i-1}+1} = t_i\),那么 \(nxt_{i} = nxt_{i-1} + 1\),这是最简单的情况。
但是如果 \(t_{nxt_{i-1}+1} \neq t_i\),此时上次的前缀无法匹配了,我们只好去找一个更短的前缀尝试匹配,也就是 \(nxt_{nxt_{i-1}}\)。
这一部分就是下图中前方被蓝线拦截的部分,因为它首先为前面绿色部分的 \(border\),而绿色部分是相等的,于是它又是后面绿色部分的 \(border\),于是是可以尝试匹配的。
一直这样匹配即可求出 \(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\) 的时候也可以类似的做,我们记录 \(f_i\) 表示字符串 \(1 \sim i\) 后 \(t\) 能匹配到 \(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)\)。
\(\mathcal{C.}\) 扩展
1.KMP 自动机
KMP 自动机其实就是比 KMP 算法多做了一件事,它额外求出了一个数组 \(trans_{i,j}\) 表示在第 \(i\) 个位置上往后匹配一个字符 \(j\) 会转移到什么状态。
前两种都很好理解,最后一种就是当无法匹配时,这个显然会等于 \(nxt_i\) 上一次匹配 \(c\) 的结果,于是直接 \(trans_{i,j} = trans_{nxt_i, j}\),而且 \(trans_{nxt_i, j}\) 显然是已经之前求出来了的结果,因为 \(nxt_i < 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\) 位的十进制数。
\(n \leq 10^9, m \leq 20\)。
我们考虑一个 \(dp\),\(f_{i, j}\) 表示当前为 \(n\) 位十进制数的第 \(i\) 位,和 \(m\) 位十进制数匹配到了第 \(j\) 位。
然后我们在额外求一个 \(g_{i, j}\) 表示上次匹配长度为 \(i\),在后面加入一个字符使得匹配长度为 \(j\) 的方案数为 \(g_{i,j}\)。
然后 \(g\) 求解就是一个 KMP 自动机就可以做完的事情。
\(f_{i,j} = \sum_{p=0}^{m-1} f_{i-1,p} \times g_{p, j}\)。
\(ans = \sum_{i = 0} ^{m-1} f_{n, 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|\),若 $s_i = s_{i + p} $, \(\forall i \in \{ 1, 2, \dots, |s| - p\}\),那么 \(p\) 为 \(s\) 的周期。
反过来,如果 \(p\) 为 \(s\) 的周期,那么 \(pre(s, |s| - p)\) 就为 \(s\) 的一个 \(border\)。
\(pre(s, k)\): 表示 \(s_{1\sim k}\)。
\(suf(s, k)\): 表示 \(s_{|s| - k + 1 \sim |s|}\)。
周期和 \(border\) 之间:
-
\(3\) 和 \(6\) 都是 \(\textbf{abcabcab}\) 的周期。
-
\(\textbf{abcab}\) 和 \(\textbf{ab}\) 都是 \(\textbf{abcabcab}\) 的 \(border\)。
-
\(pre(s, k)\) 是 \(s\) 的 border \(\leftrightarrow\) \(|s| - k\) 是 \(s\) 的周期,如下图。
border 的传递性
-
串 \(s\) 是 \(t\) 的 \(border\),串 \(t\) 是 \(r\) 的 \(border\),那么 \(s\) 是 \(r\) 的 \(border\)。例子:\(\textbf{aba}\) 为 \(\textbf{abababa}\) 的 \(border\),\(\textbf{ababa}\) 为 \(\textbf{abababa}\) 的 \(border\),那么 \(\textbf{aba}\) 是 \(\textbf{ababa}\) 的 \(border\)。
-
串 \(s\) 是 \(r\) 的 \(border\),串 \(t\) (\(|t| > |s|\)) 也是 \(r\) 的 \(border\),那么 \(s\) 是 \(t\) 的 \(border\)。
Fail 树
我们做了 KMP 之后的 \(nxt_n\) 就是字符串 \(s\) 的最长 \(border\)。
则 \(nxt_n, nxt_{nxt_n}, \dots\) 是 \(s\) 的所有 \(border\) 集合。
\(s\) 的所有 \(border\) 环环相扣,被 \(1\) 条链串起来。
而如果我们将字符串中每一个位置 \(i\) 的 \(nxt_i\) 作为 \(i\) 的父亲连边。
那么最终得到了一个以 \(0\) 为根的树,这个数被称为 \(fail\) 树。
这样他会满足如下性质:
-
点 \(i\) 的所有祖先都是前缀 \(pre(s, i)\) 的 \(border\)
-
\(s_{1\sim i}\) 和 \(s_{1 \sim j}\) 的最长公共 \(border\) 为 Fail 树上两者的 \(lca\)(注意要特判一下 \(lca\) 为 \(i\) 或 \(j\) 的情况,需要再跳一次父亲,因为一个字符串的 \(border\) 不能是他自己)。
弱周期引理
若 \(p, q\) 为 \(s\) 的周期,且 \(|p| + |q| \leq |s|\),则 \(\gcd(p, q)\) 也为 \(s\) 的周期。
证明:
设 \(p < q\),记 \(d = p - q\)。
当 \(i < q\) 时, \(s_i = s_{i + q} = s_{i + q - p} = s_{i + d}\)。
当 \(i > p\) 时, \(s _ i = s_{i - p} = s_{i + q - p} = s_{i + d}\)。
因此 \(i\) 可以取遍 \([1, |s|]\),所以 \(d\) 也是 \(s\) 的周期,根据辗转相除法,我们可以知道 \(\gcd(p, d)\) 也为 \(s\) 的周期。
一个扩展是如果 \(|p| + |q| + \gcd(|p|, |q|) \leq |s|\),那么 \(\gcd(p, q)\) 也为 \(s\) 的周期,证明不太会.jpg
前缀整除周期传递性
若 \(s\) 为 \(t\) 的前缀,且 \(t\) 存在一个周期 \(a\),\(s\) 存在一个周期 \(b\),且 \(b | a\),\(|s| \geq a\),那么 \(T\) 也有周期 \(b\)。
啊,还没写完,你先别急。