字符串算法总结
KMP
求border
for (int i = 2, j = 0; i <= m; i++) { while (b[i] != b[j + 1] && j) j = nex[j]; if (b[i] == b[j + 1]) j++; nex[i] = j; }
AC自动机 自动AC的机器
相当于$KMP+Trie$,用于求多个模式串的匹配
//构造 inline void ACbuild() { queue<int> que; for (int i = 0; i < 26; i++) if (trie[0][i]) que.push(trie[0][i]); while (que.size()) { int u = que.front(); que.pop(); for (int i = 0; i < 26; i++) { if (trie[u][i]) { last[trie[u][i]] = trie[last[u]][i]; que.push(trie[u][i]); } else trie[u][i] = trie[last[u]][i]; } } }
后缀数组 SA
将字符串的$1\leq i\leq n,s[i,n]$按照从小到大排序,用倍增和计数排序
$O(nlogn)$
int main() { n = strlen(str + 1); register int i, w, p, m = 300; for (i = 1; i <= n; ++i) cnt[rk[i] = str[i]]++; for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1]; for (i = n; i; --i) sa[cnt[rk[i]]--] = i; for (w = 1;; w <<= 1, m = p) { for (p = 0, i = n; i > n - w; --i) id[++p] = i; for (i = 1; i <= n; ++i) if (sa[i] > w) id[++p] = sa[i] - w; memset(cnt, 0, sizeof(cnt)); for (int i = 1; i <= n; ++i) ++cnt[px[i] = rk[id[i]]]; for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1]; for (i = n; i; --i) sa[cnt[px[i]]--] = id[i]; memcpy(last, rk, sizeof(rk)); for (p = 0, i = 1; i <= n; ++i) rk[sa[i]] = (last[sa[i]] == last[sa[i - 1]] && last[sa[i] + w] == last[sa[i - 1] + w]) ? p : ++p; if (p == n) { for (int i = 1; i <= n; ++i) sa[rk[i]] = i; break; } } }
用途:
寻找最小的循环移动位置,例题[JSOI2007]字符加密
在主串T中在线寻找模式串S,将T后缀数组排序后在里面二分查找S
从字符串首尾提取字符最小化字典序,例题[USACO07DEC] Best Cow Line
$height$数组
$height[i]=lcp(sa[i],sa[i-1])$,第i名的后缀与它前一名的后缀的LCP,$height[1]=0$
$O(n)$求$height$
for (int i = 1, k = 0; i <= n; i++) { if (k) k--; while (s[i + k] == s[sa[rk[i] - 1] + k]) k++; height[rk[i]] = k; }
$height$数组的用处
求两子串的最长公共前缀:$LCP(sa[i],sa[j])=min(height[i+1...j])$,之后就可以$RMQ$(区间最大最小值)解决
比较两子串大小,若要比较的是$A=S[a...b],B=S[c...d]$,若$LCP(a,c)\geq min(\left | A \right |,\left | B \right |),A<B\Leftrightarrow \left | A \right | < \left | B \right |$,否则$A<B \Leftrightarrow rk[a]<rk[b]$
求不同子串数目,$\frac{n(n+1)}{2}-\sum_{i=2}^{n}height[i]$
出现至少k次的子串的最大长度,直接看题[USACO06DEC] Milk Patterns
后缀自动机(SAM)
构造模板
inline void add(int c) { int p = last, np = last = ++tot; s[tot] = 1; d[np].len = d[p].len + 1; for (; p && !d[p].ch[c]; p = d[p].fa) d[p].ch[c] = np; if (!p) d[np].fa = 1; else { int q = d[p].ch[c]; if (d[q].len == d[p].len + 1) d[np].fa = q; else { int nq = ++tot; d[nq] = d[q]; d[nq].len = d[p].len + 1; d[q].fa = d[np].fa = nq; for (; p && d[p].ch[c] == q; p = d[p].fa) d[p].ch[c] = nq; } } } int main() { scanf("%s", str + 1); n = strlen(str + 1); for (int i = 1; i <= n; i++) add(str[i] - 'a' + 1); }
广义后缀自动机
后缀树(suffixTree)
将字符串 $S$ 的所有后缀构成的压缩 $Trie$ ,压缩 $Trie$ 指将没有分支的链压缩成一个节点的 $Trie$
struct suffixTree { int n, tot, now, rem; // n 已插入的字符数 tot 后缀树上节点数 now 当前走到的节点 rem 剩余未插入的后缀长度 int ch[N][30]; // 树上节点的边 int link[N], len[N], start[N], s[N]; // link 后缀连接 len 节点所代表字符串长度 // start 节点所代表字符串的首字母在原串中的下标 s 表示字符串原串 // 每个节点 v 所代表的字符串即为 str[start[v],start[v]+len[v]-1] // 若len[v]=inf 则表示该节点代表从 start[v] 直到最后的字符串 int new_node(int sta, int l) // 建立新节点 { link[++tot] = 1, len[tot] = l, start[tot] = sta; return tot; } void extend(int x) // 插入 x 字符 { s[++n] = x, rem++; for (int last = 1; rem;) { while (rem > len[ch[now][s[n - rem + 1]]]) rem -= len[now = ch[now][s[n - rem + 1]]]; int &v = ch[now][s[n - rem + 1]]; int c = s[start[v] + rem - 1]; if (!v || x == c) // 新建一个节点或者该后缀已经存在后缀树中 { link[last] = now; last = now; if (!v) v = new_node(n, inf); else break; } else // 新节点在一条边中间 需要将边分裂 { int u = new_node(start[v], rem - 1); ch[u][c] = v; ch[u][x] = new_node(n, inf); start[v] += rem - 1, len[v] -= rem - 1; link[last] = v = u, last = u; } if (now == 1) // 插入完成一次 将未插入的串首字符弹出继续插入 rem--; else now = link[now]; } } }
Manacher 马拉车
计算$str$中最长回文子串的长度
$d1[i]$表示以$i$为中心的长度为奇数的回文子串的半径,$d2[i]$ 表示 $i$ 为中心的长度为偶数的回文子串的半径
例:下标以 $1$ 开始的 $str=[a,b,c,b,b,c]$中,$d1[3]=2:[b,c,b]$,$d2[5]=2:[c,b,b,c]$
for (int l = 1, r = -1, i = 1; i <= n; i++) { int k = i > r ? 1 : min(d1[l + r - i], r - i); while (0 < i - k && i + k <= n && str[i - k] == str[i + k]) k++; d1[i] = k--; if (i + k > r) l = i - k, r = i + k; ans = max(ans, (d1[i] - 1) * 2 + 1); } for (int l = 1, r = -1, i = 1; i <= n; i++) { int k = i > r ? 0 : min(d2[l + r - i + 1], r - i + 1); while (0 < i - k - 1 && i + k <= n && str[i - k - 1] == str[i + k]) k++; d2[i] = k--; if (i + k > r) l = i - k - 1, r = i + k; ans = max(ans, d2[i] * 2); }
回文自动机
看wiki吧,解释不清
最小表示法
若两个字符串 $A,B$ ,有 $A[i~n]+A[1~i-1]=B$ 则称两个字符串循环同构
最小表示法便是求与字符串循环同构的字典序最小的字符串
inline int get_min(char s[]) { int k = 0, i = 0, j = 1; while (k < n && i < n && j < n) { if (s[(i + k) % n] == s[(j + k) % n]) k++; else { if (s[(i + k) % n] > s[(j + k) % n]) i = i + k + 1; else j = j + k + 1; if (i == j) j++; k = 0; } } return min(i, j); }
扩展KMP
求母串的每一个后缀与字串的公共前缀 $LCP$ ,$extend[i]$ 表示母串以 $i$ 开头的后缀与字串的 $LCP$ ,$nex[i]$ 表示字串以 $i$ 开头的后缀与字串的 $LCP$
模板:【LuoguP5410】
// s母串 t字串 inline void get_nex() { nex[0] = m; int dqx = 0; while (t[dqx] == t[dqx + 1] && dqx + 1 < m) dqx++; nex[1] = dqx; int p0 = 1; for (int i = 2; i < m; i++) { if (i + nex[i - p0] < nex[p0] + p0) nex[i] = nex[i - p0]; else { dqx = nex[p0] + p0 - i; dqx = max(dqx, 0); while (t[dqx] == t[dqx + i] && dqx + i < m) dqx++; nex[i] = dqx; p0 = i; } } } inline void get_exkmp() { get_nex(); int dqx = 0; while (s[dqx] == t[dqx] && dqx < min(n, m)) dqx++; extend[0] = dqx; int p0 = 0; for (int i = 1; i < n; i++) { if (i + nex[i - p0] < extend[p0] + p0) extend[i] = nex[i - p0]; else { dqx = extend[p0] + p0 - i; dqx = max(dqx, 0); while (t[dqx] == s[dqx + i] && dqx < m && dqx + i < n) dqx++; extend[i] = dqx; p0 = i; } } }