字符串算法
KMP
给定串 $\mathrm{s}$ 与 $\mathrm{t}$,求 $\mathrm{t}$ 在 $\mathrm{s}$ 中所有出现位置.
这个问题显然可以用 $\mathrm{SAM}/\mathrm{AC}$ 自动机处理,但 $\mathrm{KMP}$ 的处理更简单一些.
定义串 $\mathrm{t}$ 的 $\mathrm{border}$ 为最长的 $\mathrm{k}$ 使得长度为 $\mathrm{k}$ 的前缀和长度为 $\mathrm{k}$ 的后缀相等.
假设 $s$ 在 $\mathrm{i-1}$ 的位置上匹配到了 $\mathrm{t}$ 的位置 $\mathrm{j}$.
那么假如说在 $\mathrm{i}$ 的位置上与 $\mathrm{t}$ 在 $\mathrm{j+1}$ 的位置上不相等的话 $\mathrm{t}$ 的下一个匹配位置就是 $\mathrm{border(j)}$.
在 KMP 中求 $\mathrm{border}$ :
void get_nex() { nex[1] = 0; for(int i = 2,j = 0; i <= m ; ++ i) { while(j && B[j + 1] != B[i]) j = nex[j]; if(B[j + 1] == B[i]) ++ j ; nex[i] = j; } }
这个复杂度是均摊 $\mathrm{O(m)}$ 的,因为向后移动最多 $\mathrm{m}$ 次,所以向前跳 $\mathrm{nex}$ 也最多 $\mathrm{m}$ 次.
与模式串进行匹配:
for(int i = 1, j = 0; i <= n ; ++ i) { while(j && B[j + 1] != A[i]) j = nex[j]; if(B[j + 1] == A[i]) ++ j; if(j == m) { printf("%d\n",i - m + 1); j = nex[j]; } }
例题:OKR-Periods of Words
来源:洛谷P3435 [POI2006]OKR-Periods of Words
手画一下发现对于前缀 $\mathrm{i}$ 来说任意一个“周期”可以表示成 $\mathrm{i-border(i)}$.
由于要最大化周期,故需要 $\mathrm{border(i)}$ 越小越好.
求完 $\mathrm{next}$ 数组后从前向后扫一遍即可.
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> #define N 1000009 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; char A[N]; int n, nex[N]; int main() { // setIO("input"); scanf("%d", &n); scanf("%s", A + 1); for(int i = 2, j = 0; i <= n ; ++ i) { while(A[i] != A[j + 1] && j) j = nex[j]; if(A[i] == A[j + 1]) ++ j; nex[i] = j; } for(int i = 1; i <= n ; ++ i) { if(nex[nex[i]]) nex[i] = nex[nex[i]]; } ll ans = 0; for(int i = 1; i <= n ; ++ i) { if(nex[i]) ans += i - nex[i]; } printf("%lld\n", ans); return 0; }
SAM
后缀自动机是处理字符串问题最有力的工具之一.
原理实在太过于复杂了,这里列几条非常有用的定理和性质.
1. 后缀树的形态就是反串的 $\mathrm{trie}$ 树的样子.
SA
在处理大型字符串问题时,后缀自动机是最常用的方法.
但是在一些时候,后缀数组会更加方便.
比如说 $\mathrm{SA}$+单调栈裸题需要用 $\mathrm{SAM}$ 和虚树实现.
后缀数组模板主要用来求 $\mathrm{sa}$ 数组和 $\mathrm{rank}$ 数组.
其中,$\mathrm{sa[i]}$ 表示排名为 $\mathrm{i}$ 的后缀所在位置, $\mathrm{rank[i]}$ 表示 $\mathrm{i}$ 的名次.
显然,有 $\mathrm{rank[sa[i]]=i, sa[rank[i]]=i}$.
求解过程运用的是倍增算法以及二元组排序.
假设当前要对所有 $\mathrm{2k}$ 的子串排序,且已经排好长度为 $\mathrm{k}$ 的所有子串.
那么对于每一个 $\mathrm{2k}$ 的子串都可以分为前一半和后一半.
前一半在上一次的排名为第一关键字,后一半在前一次的排名为第二关键字.
那么在合并的时候对于第一关键字相同的部分就按照第二关键字来排序.
for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = n; i >= 1; -- i) { sa[t[rk[sec[i]]] -- ] = sec[i], sec[i] = 0; }
这段代码就是问题的核心.
完整代码:
#include <bits/stdc++.h> #define N 2000009 #define setIO(s) freopen(s".in","r",stdin) #define ll long long using namespace std; char str[N]; int n, sa[N], rk[N], t[N], sec[N], ty; void get_sa() { int cnt = 0; for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i] = str[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = 1; i <= n ; ++ i) sa[t[rk[i]] --] = i; for(int k = 1; k <= n ; k <<= 1) { cnt = 0; for(int i = n - k + 1; i <= n ; ++ i) sec[++cnt] = i; for(int i = 1; i <= n ; ++ i) { if(sa[i] > k) sec[++cnt] = sa[i] - k; } for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = n; i >= 1; -- i) { sa[t[rk[sec[i]]] -- ] = sec[i], sec[i] = 0; } swap(sec, rk); rk[sa[1]] = cnt = 1; for(int i = 2; i <= n ; ++ i) { rk[sa[i]] = (sec[sa[i - 1]] == sec[sa[i]] && sec[sa[i - 1] + k] == sec[sa[i] + k]) ? cnt : ++ cnt; } ty = cnt; if(cnt == n) break; } } int main() { // setIO("input"); scanf("%s", str + 1); n = strlen(str + 1), ty = 134; get_sa(); for(int i = 1; i <= n ; ++ i) { printf("%d ", sa[i]); } return 0; }
后缀数组一个最重要的应用就是求 $\mathrm{h[x]}$ 数组.
其中 $\mathrm{h[i]}$ 的定义就是后缀 $\mathrm{i}$ 与排名为 $\mathrm{rank[i]-1}$ 的后缀的 $\mathrm{lcp}$.
有结论 $\mathrm{h[i-1]-1 \leqslant h[i]}$
令 $\mathrm{height[rk[x]]=h[x]}$.
那么任意两个后缀的 $\mathrm{lcp}$ 就等于 $\mathrm{(rk[i], rk[j]]}$ 的区间最小值.
int det = 0; for(int i = 1; i <= n ; ++ i) { // h[i] : rank[i] 与 rank[i] - 1 的 LCP. if(det) -- det; while(str[i + det] == str[sa[rk[i] - 1] + det]) ++ det; h[rk[i]] = det; }
在很多时候,需要在 $\mathrm{h[i]}$ 数组基础上构建主席树来统计信息.
一般来说,会先用二分法确定一段区间(即 $\mathrm{LCP}$ 大于等于某个值)
然后再对位置信息构建主席树,并进行统计(空间不够就离线+线段树)
例题:
生成魔咒
来源:bzoj4516. [Sdoi2016]生成魔咒
利用后缀数组求本质不同子串时,每一个后缀的初始贡献就是后缀长度.
然后再减掉 $\mathrm{h[i]}$, 即 $\mathrm{i-1}$ 前已经计算过的部分.
这道题是动态向后加,不妨将字符串翻转变成先前加入字符.
那么,等于说每次新加入一个后缀,一个 $\mathrm{h[x]}$.
那么只需动态加入后缀,并维护新加入的后缀和之前后缀的 $\mathrm{lcp}$ 即可.
#include <cstdio> #include <set> #include <vector> #include <cstring> #include <algorithm> #define ll long long #define pb push_back #define N 200009 #define setIO(s) freopen(s".in","r",stdin) using namespace std; set<int>se; set<int>::iterator it; int Lg[N]; int arr[N], sa[N], rk[N], t[N], mi[N][18], A[N], h[N], sec[N], n, si, ty; void get_sa() { int cnt = 0; for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i] = A[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = 1; i <= n ; ++ i) sa[t[rk[i]] --] = i; for(int k = 1; k <= n ; k <<= 1) { cnt = 0; for(int i = n - k + 1; i <= n ; ++ i) sec[++ cnt] = i; for(int i = 1; i <= n ; ++ i) { if(sa[i] > k) sec[++ cnt] = sa[i] - k; } for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) ++t[rk[i]]; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = n; i >= 1; -- i) { sa[t[rk[sec[i]]] -- ] = sec[i], sec[i] = 0; } swap(rk, sec); rk[sa[1]] = cnt = 1; for(int i = 2; i <= n ; ++ i) { rk[sa[i]] = (sec[sa[i - 1]] == sec[sa[i]] && sec[sa[i - 1] + k] == sec[sa[i] + k]) ? cnt : ++ cnt; } ty = cnt; if(cnt == n) { break; } } int det = 0; for(int i = 1; i <= n ; ++ i) { if(det) -- det; while(A[i + det] == A[sa[rk[i] - 1] + det]) ++ det; h[rk[i]] = det; } // 预处理完毕. } void get_rmq() { Lg[1] = 0; for(int i = 2; i <= n ; ++ i) Lg[i] = Lg[i >> 1] + 1; for(int i = 1; i <= n ; ++ i) mi[i][0] = h[i]; for(int i = 1; i < 18 ; ++ i) { for(int j = 1; j + (1 << i) - 1 <= n ; ++ j) mi[j][i] = min(mi[j][i - 1], mi[j + (1 << (i - 1))][i - 1]); } } int query(int l, int r) { int p = Lg[r - l + 1]; return min(mi[l][p], mi[r - (1 << p) + 1][p]); } int main() { // setIO("input"); scanf("%d", &n); for(int i = 1; i <= n ; ++ i) { scanf("%d", &arr[i]); A[i] = arr[i]; } sort(arr + 1, arr + 1 + n); si = unique(arr + 1, arr + 1 + n) - (arr + 1); for(int i = 1; i <= n ; ++ i) { A[i] = lower_bound(arr + 1, arr + 1 + si, A[i]) - arr; } reverse(A + 1, A + 1 + n); ty = n, get_sa(), get_rmq(); ll ans = 0; for(int i = n; i >= 1; -- i) { // 不断加入后缀. se.insert(rk[i]); it = se.find(rk[i]); int k = 0; if(it != se.begin()) { it -- ; k = query((*it) + 1, rk[i]); it ++ ; } it ++ ; if(it != se.end()) k = max(k, query(rk[i] + 1, (*it))); ans += n - i + 1 - k; printf("%lld", ans); if(i != 1) { printf("\n"); } } return 0; }
品酒大会
来源:bzoj4199. [Noi2015]品酒大会
不妨考虑求 $\mathrm{lcp}$ 恰好为 $\mathrm{k}$ 的答案,然后最后来一个后缀和求答案.
考虑用单调栈解决这个问题,但是在后缀数组中 $\mathrm{lcp}$ 的长度是 $\mathrm{min(l,r]}$, 即一个左开右闭区间.
所以在用单调栈统计答案的时候对于 $\mathrm{[l,r]}$ 这段区间的值计为 $\mathrm{h[r+1]}$, 这样可以求解.
然后最后在加入 $\mathrm{n}$, 由于 $\mathrm{n-1}$ 加入的时候的值是按照 $\mathrm{n}$ 算的,所以可以直接加进去.
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> #define N 600009 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; const ll inf = 2000000000000000000ll; char str[N]; ll ans1[N], ans2[N], ans3[N]; int n, ty, val[N], sa[N], t[N], h[N], rk[N], sec[N]; void get_sa() { int cnt = 0; for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i] = str[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = 1; i <= n ; ++ i) sa[t[rk[i]] --] = i; for(int k = 1; k <= n ; k <<= 1) { int cnt = 0; for(int i = n - k + 1; i <= n ; ++ i) sec[++ cnt] = i; for(int i = 1; i <= n ; ++ i) { if(sa[i] > k) sec[++ cnt] = sa[i] - k; } for(int i = 1; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n; ++ i) t[rk[i]] ++; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = n; i >= 1; -- i) { sa[t[rk[sec[i]]] --] = sec[i], sec[i] = 0; } swap(rk, sec); rk[sa[1]] = cnt = 1; for(int i = 2; i <= n ; ++ i){ rk[sa[i]] = (sec[sa[i - 1]] == sec[sa[i]] && sec[sa[i - 1] + k] == sec[sa[i] + k]) ? cnt : ++ cnt; } ty = cnt; if(cnt == n) { break; } } int det = 0; for(int i = 1; i <= n ; ++ i) { if(det) -- det; while(str[i + det] == str[sa[rk[i] - 1] + det]) ++ det; h[rk[i]] = det; } } int top; int sta[N], mi[N], ma[N], si[N], st[N]; int main() { // setIO("input"); scanf("%d", &n); scanf("%s", str + 1), ty = 134; for(int i = 1; i <= n ; ++ i) { scanf("%d", &val[i]); } get_sa(); for(int i = 1; i <= n ; ++ i) { ans2[i] = -inf; } for(int i = 2; i <= n ; ++ i) { int minn = val[sa[i - 1]], maxx = val[sa[i - 1]], k = 1; while(top >= 1 && h[i] <= st[top]) { ans1[st[top]] += 1ll * si[top] * k; ans2[st[top]] = max(ans2[st[top]], max(1ll * mi[top] * minn, 1ll * ma[top] * maxx)); k += si[top]; minn = min(minn, mi[top]); maxx = max(maxx, ma[top]); -- top; } st[++ top] = h[i]; ma[top] = maxx; mi[top] = minn; si[top] = k; } // 现在加入 n; int k = 1, minn = val[sa[n]], maxx = val[sa[n]]; for(int i = top; i >= 1; -- i) { ans1[st[i]] += 1ll * si[i] * k; ans2[st[i]] = max(ans2[st[i]], max(1ll * mi[i] * minn, 1ll * ma[i] * maxx)); minn = min(minn, mi[i]); maxx = max(maxx, ma[i]); k += si[i]; } for(int i = n - 1; i >= 1; -- i) { ans1[i] += ans1[i + 1]; if(ans1[i + 1]) ans2[i] = max(ans2[i], ans2[i + 1]); } ll min1 = (ll)val[1], min2 = (ll)val[2], max1 = (ll)val[1], max2 = (ll)val[2]; for(int i = 3; i <= n ; ++ i) { if(val[i] < min1) min2 = min1, min1 = val[i]; else min2 = min(min2, 1ll * val[i]); if(val[i] > max1) max2 = max1, max1 = val[i]; else max2 = max(max2, 1ll * val[i]); } printf("%lld %lld\n", 1ll * n * (n - 1) / 2, max(max1 * max2, min1 * min2)); for(int i = 1; i < n ; ++ i) { printf("%lld ", ans1[i]); if(ans1[i]) printf("%lld\n", ans2[i]); else printf("0\n"); } return 0; }
PAM
回文自动机是处理回文串有力的工具.
和后缀自动机类似,回文自动机能够存储一个字符串所有本质不同的回文串.
这里特别注意:一个串本质不同的回文子串最多有 $O(n)$ 种.
于是,完全可以做到让回文自动机的每个节点表示一个本质不同的回文子串.
1.$\mathrm{ch[x][c]}$ 表示在 $\mathrm{x}$ 代表的回文串中左右两端加入 $\mathrm{c}$.
2.$\mathrm{pre[x]}$ 指向 $\mathrm{x}$ 的最长回文后缀的节点.
由于回文串有奇数和偶数之分,于是我们设置 $1$ 号点的长度为 $\mathrm{-1}$.
这样一来,在 $1$ 号点的左右两端加入字符就相当于只加入了 1 个字符.
初始化:
void init() { pre[0] = 1, len[0] = 0; pre[1] = 1, len[1] = -1; tot = last = 1; }
转移函数:
int trans(int p, int pos) { while((S[pos] != S[pos - len[p] - 1]) || (pos - len[p] - 1 < 1)) { p = pre[p]; } return p ; }
插入一个字符:
void insert(int c, int pos) { int p = trans(last, c, pos); if(!ch[p][c]) { len[++ tot] = len[p] + 2; pre[tot] = ch[trans(pre[p], c, pos)][c]; ch[p][c] = tot; dep[tot] = dep[pre[tot]] + 1; } last = ch[p][c]; }
这个 $\mathrm{dep[x]}$ 记录以 $\mathrm{x}$ 为右端点的回文串数量.
Manacher
$\mathrm{Manacher}$ 算法可以求每一个 $\mathrm{i}$ 为回文中心的回文串长度,这是 $\mathrm{PAM}$ 做不到的.
算法的核心是维护一个 $\mathrm{mid}$, 表示右端点最大的回文中心,然后每次比对 $\mathrm{i}$ 与 $\mathrm{mx}$ 的关系.
void getstr() { int cnt = 0; ss[++ cnt] = '#'; for(int i = 1; i <= n ; ++ i) { ss[++ cnt] = str[i], ss[++ cnt] = '#'; } n = cnt; } void Manacher() { int mx = 0, mid = 0; for(int i = 1; i <= n ; ++ i) { if(i <= mx) p[i] = min(p[2 * mid - i], mx - i + 1); else p[i] = 1; while(i + p[i] <= n && i - p[i] >= 1 && ss[i + p[i]] == ss[i - p[i]]) ++ p[i]; // 得到 p[i] if(i + p[i] - 1 > mx) mx = i + p[i] - 1, mid = i; } }