OI+ACM 笔记:B - 字符串
B - 字符串
Hash & Hash 表
- 在设计有多关键字的 hash 函数时,应确保每一个关键字都得以体现。
- 在 hash 值不同时,两个数据一定不一样;在 hash 值相同时,两个数据有极大概率一样。
字符串哈希值:\(f(s) = \sum s_i \cdot \mathrm{base}^{l - i} \pmod M\)。hash 的模数 \(M\) 要比最大字符大,最好为质数。
子串哈希值:
KMP
参考 1:https://www.luogu.com.cn/blog/command-block/border-li-lun-xiao-ji。
参考 2:https://www.luogu.com.cn/blog/ix-35/noi-yi-lun-fu-xi-ii-zi-fu-chuan。
字符串的周期:对字符串 \(s\) 与 \(0 < p \leq |s|\),若 \(s_i = s_{i + p}\) 对所有 \(i \in [1, |s| - p]\) 都成立,则称 \(p\) 为 \(s\) 的周期。特别地,若 \(|s|\) 整除 \(p\),则称 \(p\) 为 \(s\) 的整周期。
字符串的 border:对字符串 \(s\) 与 \(0 \leq r < |s|\),若 \(s\) 长度为 \(r\) 的前缀和后缀相等,就称 \(s\) 长度为 \(r\) 的前缀为 \(s\) 的 border。
前缀函数 \(\text{next}_i\):字符串 \(s[1 : i]\) 的最长 border 的长度。
- 性质:\(s\) 有长度为 \(r\) 的 border \(\iff\) \(|s| - r\) 是 \(s\) 的周期。
- 性质:\(s\) 的最长 border 接上该最长 border 的所有 border 恰好为 \(s\) 的所有 border。
- 故 \(s[1 : i]\) 的所有 border 为 \(\mathrm{next}_i, \mathrm{next}_{\mathrm{next}_i}, \cdots\)
- 性质(弱周期引理):若 \(p, q\) 是 \(s\) 的周期,且 \(p + q \leq |s|\),则 \(\gcd(p, q)\) 也是 \(s\) 的周期。
- 性质(周期引理):若 \(p, q\) 是 \(s\) 的周期,且 \(p + q - \gcd(p, q) \leq |s|\),则 \(\gcd(p, q)\) 也是 \(s\) 的周期。
- 性质:若 \(S\) 是 \(T\) 的前缀,\(S\) 有整周期 \(a\),\(T\) 有周期 \(b\),\(a \mid b\),\(b \leq |S|\),则 \(T\) 也有周期 \(a\)。
- 性质:若 \(S\) 在 \(T\) 中的某两个匹配 \(S_1, S_2\) 有重合部分,且起始位置相差 \(d\),则 \(S\) 与 \(S_1 \bigcup S_2\) 均有周期 \(d\)。
- 性质:若 \(|S| \geq \frac{|T|}{2}\),则 \(S\) 在 \(T\) 中的匹配起始位置形成等差数列。
- 性质(短周期结构):\(s\) 的所有不大于 \(\frac{|s|}{2}\) 的周期,都是其最短周期的倍数。
- 性质(长 border 结构):\(s\) 的所有不小于 \(\frac{|s|}{2}\) 的 border,长度构成一个等差数列。
- 性质:\(s\) 的所有 border 按长度排序后,可以被划分成 \(\mathcal{O}(\log |s|)\) 个等差数列。
- 性质:\(s\) 的所有公差 \(\geq d\) 的 border 的等差数列总大小是 \(\mathcal{O}(\frac{n}{d})\) 的。
int net[N];
void kmp_init() {
net[1] = 0;
for (int i = 2, j = 0; i <= m; i ++) {
while (j > 0 && b[j + 1] != b[i]) j = net[j];
if (b[j + 1] == b[i]) j ++;
net[i] = j;
}
}
int f[N];
void kmp() {
for (int i = 1, j = 0; i <= n; i ++) {
while (j > 0 && (j == m || b[j + 1] != a[i])) j = net[j];
if (b[j + 1] == a[i]) j ++;
f[i] = j;
}
}
Z 算法
Z 函数:字符串 \(s\) 和 \(s[i : n]\) 的最长公共前缀(LCP)长度,记作 \(Z_i\)。特别地,\(Z_1 = 0\)。
流程:考虑从前往后扫。维护一个右端点最靠右的、和前缀匹配的子串。不妨记作 \(b[l, r]\)。
- 若 \(r < i\),则
- 若 \(r \geq i\),则
时间复杂度:注意到单次枚举的次数为 \(\max\{l + Z_l - 1\}\) 的增量,故时间复杂度为 \(\mathcal{O}(n)\)。
int Z[N];
void Z_init() {
Z[1] = 0;
for (int i = 2, l = 0, r = 0; i <= m; i ++) {
if (r < i)
Z[i] = 0;
else
Z[i] = std::min(Z[i - l + 1], r - i + 1);
while (i + Z[i] <= m && b[1 + Z[i]] == b[i + Z[i]]) Z[i] ++;
if (i + Z[i] - 1 > r)
l = i, r = i + Z[i] - 1;
}
}
int p[N];
void Z_algorithm() {
for (int i = 1, l = 0, r = 0; i <= n; i ++) {
if (r < i)
p[i] = 0;
else
p[i] = std::min(Z[i - l + 1], r - i + 1);
while (i + p[i] <= n && b[1 + p[i]] == a[i + p[i]]) p[i] ++;
if (i + p[i] - 1 > r)
l = i, r = i + p[i] - 1;
}
}
Manacher
转化:回文串按照长度分成 奇 / 偶 两类,奇回文串的对称轴处于一个字符上,偶回文串的对称轴处于两个相邻字符的中间。可以在一个字符串的首尾以及所有的相邻两字符之间插入一个原串中未出现的字符,如 #, $, ^
。这样原串中每一个回文串,都可以对应到新串中每一个极长的奇回文串。
数组 \(p_i\):以 \(s_i\) 为中心的最长回文子串的半径(含中心)。
流程:考虑从前往后扫。维护一个右端点最靠右的回文子串,不妨记该回文串的中心为 \(k\)。
- 若 \(k + p_k - 1 < i\),则
- 若 \(k + p_k - 1 \geq i\),则
时间复杂度:注意到单次枚举的次数为 \(\max\{k + p_k - 1\}\) 的增量,故时间复杂度为 \(\mathcal{O}(n)\)。
const int SIZE = N * 2;
int m, b[SIZE];
void manacher_init() {
m = 0;
b[++ m] = '#';
for (int i = 1; i <= n; i ++) b[++ m] = a[i], b[++ m] = '#';
b[0] = '(', b[m + 1] = ')';
}
int p[SIZE];
void manacher() {
p[1] = 1;
for (int i = 2, k = 1; i <= m; i ++) {
if (k + p[k] - 1 < i)
p[i] = 1;
else
p[i] = std::min(k + p[k] - i, p[k * 2 - i]);
while (b[i - p[i]] == b[i + p[i]]) p[i] ++;
if (i + p[i] > k + p[k]) k = i;
}
}
Trie
trie:trie 是一个纯正的自动机,形态是树。一个起点,若干终点。多模式串的所有前缀,与 trie 上的所有状态一一对应。
trie 中的边:trie 主要考虑一种转移边,表示在某个状态所表示的串后面加一个字符。
namespace trie {
int nClock;
struct node {
int trans[26];
} t[N];
void insert(char *s) {
int p = 1, len = strlen(s);
for (int i = 0; i < len; i ++) {
int v = s[i] - 'a';
if (!t[p].trans[v]) t[p].trans[v] = ++ nClock;
p = t[p].trans[v];
}
}
}
- 0/1 trie 支持持久化,可以作为 0/1 trie 的前缀和来使用。
namespace trie {
const int pond = ...;
int nClock, root[N];
struct node {
int trans[2];
int cnt;
} t[pond];
void insert(int &p, int q, int dep, int x) {
p = ++ nClock, t[p] = t[q];
t[p].cnt ++;
if (dep < 0) return;
int v = x >> dep & 1;
insert(t[p].trans[v], t[q].trans[v], dep - 1, x);
}
}
- 0/1 trie 合并单次复杂度,取决于两棵 0/1 trie 的交集大小;0/1 trie 合并总复杂度,取决于所有 0/1 trie 的节点总数。
- 0/1 trie 合并支持可持久化,需要在合并时新建节点。
namespace trie {
const int pond = ...;
int nClock, root[N];
struct node {
int trans[2];
int cnt;
#define lc trans[0]
#define rc trans[1]
} t[pond];
void insert(int &p, int dep, int x) {
if (!p) p = ++ nClock;
t[p].cnt ++;
if (dep < 0) return;
int v = x >> dep & 1;
insert(t[p].trans[v], dep - 1, x);
}
int merge(int p, int q) {
if (!p || !q) return p ^ q;
t[p].cnt += t[q].cnt;
t[p].lc = merge(t[p].lc, t[q].lc);
t[p].rc = merge(t[p].rc, t[q].rc);
return p;
}
/* 可持久化 0/1 trie 合并
int merge(int p, int q) {
if (!p || !q) return p ^ q;
int u = ++ nClock;
t[u].cnt = t[p].cnt + t[q].cnt;
t[u].lc = merge(t[p].lc, t[q].lc);
t[u].rc = merge(t[p].rc, t[q].rc);
return u;
}
*/
}
0/1 trie 中查询,与 \(x\) 异或的最大值:从高位 \(\to\) 低位贪心,有异走异,无异走同。
namespace trie {
const int pond = ...;
int nClock, root;
struct node {
int trans[2];
} t[pond];
void insert(int &p, int dep, int x) {
if (!p) p = ++ nClock;
if (dep < 0) return;
int v = x >> dep & 1;
insert(t[p].trans[v], dep - 1, x);
}
int ask(int p, int dep, int x) {
if (dep < 0) return 0;
int v = x >> dep & 1;
if (t[p].trans[v ^ 1])
return ask(t[p].trans[v ^ 1], dep - 1, x) + (1 << dep);
else
return ask(t[p].trans[v], dep - 1, x);
}
}
0/1 trie 中查询,与 \(x\) 异或的第 \(k\) 大值:从高位 \(\to\) 低位考虑
- 设当前考虑到第 \(i\) 位,设 \(x\) 的异位里有 \(\mathrm{cnt}\) 个数:
- 若 \(k \leq \mathrm{cnt}\),则寻找 \(x\) 的异位中与 \(x\) 异或的第 \(k\) 大值。
- 若 \(k > \mathrm{cnt}\),则寻找 \(x\) 的同位中与 \(x\) 异或的第 \(k - \mathrm{cnt}\) 大值。
namespace trie {
const int pond = ...;
int nClock, root;
struct node {
int trans[2];
int cnt;
} t[pond];
void insert(int &p, int dep, int x) {
if (!p) p = ++ nClock;
t[p].cnt ++;
if (dep < 0) return;
int v = x >> dep & 1;
insert(t[p].trans[v], dep - 1, x);
}
int ask(int p, int dep, int x, int k) {
if (dep < 0) return 0;
int v = x >> dep & 1;
if (k <= t[t[p].trans[v ^ 1]].cnt)
return ask(t[p].trans[v ^ 1], dep - 1, x, k) + (1 << dep);
else
return ask(t[p].trans[v], dep - 1, x, k - t[t[p].trans[v ^ 1]].cnt);
}
}
0/1 trie 中查询,与 \(x\) 异或 \(\geq k\) 的信息:从高位 \(\to\) 低位考虑
- 设当前考虑到第 \(i\) 位:
- 若 \(k\) 的第 \(i\) 位为 \(1\),走 \(x\) 的异位。
- 若 \(k\) 的第 \(i\) 位为 \(0\),计算 \(x\) 的异位的贡献,走 \(x\) 的同位。
- 考虑完所有位之后,计算当前叶子节点的贡献。
namespace trie {
const int pond = ...;
int nClock, root;
struct node {
int trans[2];
} t[pond];
void insert(int &p, int dep, int x) {
if (!p) p = ++ nClock;
if (dep < 0) return;
int v = x >> dep & 1;
insert(t[p].trans[v], dep - 1, x);
}
int ask(int p, int dep, int x, int k) {
if (dep < 0) return // Calculate the data of node p
int v = x >> dep & 1;
if (k >> dep & 1)
return ask(t[p].trans[v ^ 1], dep - 1, x, k);
else
return ask(t[p].trans[v], dep - 1, x, k) + // Calculate the data of node t[p].trans[v ^ 1]
}
}
0/1 trie 中修改,令全局异或 \(x\):从高位 \(\to\) 低位考虑,运用懒标记,设当前考虑到第 \(i\) 位,若懒标记的第 \(i\) 位为 \(1\),则交换左右儿子。
namespace trie {
const int pond = ...;
int nClock, root;
struct node {
int trans[2];
int tag;
#define lc trans[0]
#define rc trans[1]
void mk_tag(int x, int dep) {
if (dep < 0) return;
if (x >> dep & 1) std::swap(lc, rc);
tag ^= x;
}
} t[pond];
void spread(int p, int dep) {
if (t[p].tag) {
t[t[p].lc].mk_tag(t[p].tag, dep - 1);
t[t[p].rc].mk_tag(t[p].tag, dep - 1);
t[p].tag = 0;
}
}
}
0/1 trie 中维护集合异或和,支持插入、删除、全局 +1:从低位 \(\to\) 高位考虑
- 需要维护:
- \(\mathrm{trans}\):转移边。
- \(\mathrm{cnt}\):以当前状态为根的子树内的结点数。
- \(\mathrm{xr}\):以当前状态为根的子树内的异或和。
- 插入、删除:递归操作即可。
- 全局 +1:交换当前节点的 \(0/1\) 儿子,进入 \(0\) 转移边(没交换前是 \(1\) 转移边)的儿子递归。
- 考虑二进制下 +1,相当于从低位 \(\to\) 高位找到第一个 \(0\),将其改为 \(1\),将前面的所有 \(1\) 改为 \(0\)。
namespace trie {
const int pond = ...;
int nClock, root;
struct node {
int trans[2];
int cnt;
int xr;
#define lc trans[0]
#define rc trans[1]
} t[pond];
void upd(int p) {
t[p].cnt = t[t[p].lc].cnt + t[t[p].rc].cnt;
t[p].xr = (t[t[p].lc].xr << 1) ^ (t[t[p].rc].xr << 1) ^ (t[t[p].rc].cnt & 1);
}
void insert(int &p, int dep, int x, int opt) {
if (!p) p = ++ nClock;
if (dep == logA) { t[p].cnt += opt; return; }
int v = x >> dep & 1;
insert(t[p].trans[v], dep - 1, x, opt);
upd(p);
}
void add_one(int p) {
std::swap(t[p].lc, t[p].rc);
if (t[p].lc) add_one(t[p].lc);
upd(p);
}
}
AC 自动机
AC 自动机:AC 自动机是一个状态机,形态是 trie。一个起点,若干终点。多模式串的所有前缀,与 AC 自动机上的所有状态一一对应。
AC 自动机中的边:AC 自动机主要考虑两种边:
- 普通转移边:表示在某个状态所表示的串后面加一个字符。
- 失配指针 fail:表示某个状态所表示的串的最长后缀。这类边构成一棵树,称作 fail 树。不一定满足 \(\mathrm{fail}_i < i\)。
trie 图:对于不存在的转移 \(\delta(p, c)\),指向 \(\delta(\mathrm{fail}_p, c)\)。本质上是路径压缩。
trie 图中的普通转移边:从状态 \(s\) 指向状态 \(s' + c\),其中 \(s'\) 为 \(s\) 的满足 \(s' + c\) 存在的最长后缀。
namespace AC {
int nClock = 1;
struct node {
int trans[26];
int fail;
} t[N];
void insert(char *s) {
int p = 1, len = strlen(s);
for (int i = 0; i < len; i ++) {
int v = s[i] - 'a';
if (!t[p].trans[v]) t[p].trans[v] = ++ nClock;
p = t[p].trans[v];
}
}
void build_fail() {
for (int i = 0; i < 26; i ++) t[0].trans[i] = 1;
t[1].fail = 0;
std::queue<int> q;
q.push(1);
while (q.size()) {
int u = q.front(); q.pop();
for (int i = 0; i < 26; i ++)
if (t[u].trans[i])
t[t[u].trans[i]].fail = t[t[u].fail].trans[i], q.push(t[u].trans[i]);
else
t[u].trans[i] = t[t[u].fail].trans[i];
}
}
}
namespace AC {
int nClock = 1;
struct node {
int trans[26];
int fail;
} t[N];
void insert(char *s) {
int p = 1, len = strlen(s);
for (int i = 0; i < len; i ++) {
int v = s[i] - 'a';
if (!t[p].trans[v]) t[p].trans[v] = ++ nClock;
p = t[p].trans[v];
}
}
void build_fail() {
for (int i = 0; i < 26; i ++) t[0].trans[i] = 1;
t[1].fail = 0;
std::queue<int> q;
q.push(1);
while (q.size()) {
int u = q.front(); q.pop();
for (int i = 0; i < 26; i ++)
if (t[u].trans[i])
t[t[u].trans[i]].fail = t[t[u].fail].trans[i], q.push(t[u].trans[i]);
else
t[u].trans[i] = t[t[u].fail].trans[i];
}
}
int tot, head[N], ver[N], Next[N];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
void build_tree() { // fail 树
for (int i = 2; i <= nClock; i ++)
add_edge(t[i].fail, i);
}
}
- 建 trie 图时,当字符集很大时,需要用到
std::map
与可持久化数组建 trie 图。
namespace SGT {
const int pond = ...;
int nClock;
struct node {
int lc, rc;
int val;
} t[pond];
void build(int &p, int l, int r) {
p = ++ nClock;
if (l == r) { t[p].val = 1; return; }
int mid = (l + r) >> 1;
build(t[p].lc, l, mid), build(t[p].rc, mid + 1, r);
}
void insert(int &p, int q, int l, int r, int x, int val) {
p = ++ nClock, t[p] = t[q];
if (l == r) { t[p].val = val; return; }
int mid = (l + r) >> 1;
if (x <= mid)
insert(t[p].lc, t[q].lc, l, mid, x, val);
else
insert(t[p].rc, t[q].rc, mid + 1, r, x, val);
}
int ask(int p, int l, int r, int x) {
if (l == r) return t[p].val;
int mid = (l + r) >> 1;
if (x <= mid)
return ask(t[p].lc, l, mid, x);
else
return ask(t[p].rc, mid + 1, r, x);
}
}
namespace AC {
int nClock = 1;
struct node {
std::map<int, int> trans;
int fail;
} t[N];
int root[N];
void insert(const int &n, int *a) {
int p = 1;
for (int i = 1; i <= n; i ++) {
int v = a[i];
if (!t[p].trans.count(v)) t[p].trans[v] = ++ nClock;
p = t[p].trans[v];
}
}
void build_fail() {
SGT::build(root[0], 1, m);
t[1].fail = 0;
std::queue<int> q;
q.push(1);
while (q.size()) {
int u = q.front(); q.pop();
root[u] = root[t[u].fail];
for (auto it : t[u].trans) {
int c = it.first, v = it.second;
SGT::insert(root[u], root[u], 1, m, c, v);
t[v].fail = SGT::ask(root[t[u].fail], 1, m, c);
}
}
}
}
SA
后缀数组 \(\mathrm{sa}_i\):排名为 \(i\) 的后缀。
排名数组 \(\mathrm{rk}_i\):后缀 \(i\) 的排名。
LCP 数组 \(\mathrm{height}_i\):后缀 \(\mathrm{sa}_{i - 1}, \mathrm{sa}_i\) 的最长公共前缀,即 \(\mathrm{height}_i = \mathrm{LCP}(\mathrm{sa}_{i - 1}, \mathrm{sa}_i)\)。
性质:后缀 \(\mathrm{sa}_i, \mathrm{sa}_j\) 的最长公共前缀为
性质(序列 SA):
性质(序列 SA)证明:
- 当 \(\mathrm{height}_{\mathrm{rk}_{i - 1}} = 0\) 时,命题成立。
- 当 \(\mathrm{height}_{\mathrm{rk}_{i - 1}} \geq 1\) 时,考虑到后缀排序将前缀相近的后缀放到一起,所以 \(\mathrm{height}_i\) 可以看做字典序小于 \(\mathrm{sa}_{i}\) 的后缀与 \(\mathrm{sa}_i\) 的 LCP 最大值,记 \(u = \mathrm{sa}_{\mathrm{rk}_{i - 1} - 1}\),必有 \(s_u = s_{i - 1}\),则 \(s[u + 1 : n]\) 的字典序比 \(s[i : n]\) 的字典序小,则必然存在一个后缀使得与其 LCP \(\geq \mathrm{height}_{\mathrm{rk}_{i - 1}} - 1\),故命题成立。
序列 SA 求法:
- \(\mathcal{O}(n \log^2 n)\) 求序列 SA:sort + 倍增 + hash。当比较函数时间开销较大时,使用
std::stable_sort()
。 - \(\mathcal{O}(n \log n)\) 求序列 SA:倍增 + 双关键字排序。
- \(\mathcal{O}(n)\) 求序列 SA:DC3、SA-IS。
namespace SA {
int m = 256;
int sa[N], rk[N], height[N];
int cnt[N], id[N], px[N];
int tmp_rk[N];
bool same(int x, int y, int k) {
int p = x + k <= n ? tmp_rk[x + k] : -1;
int q = y + k <= n ? tmp_rk[y + k] : -1;
return tmp_rk[x] == tmp_rk[y] && p == q;
}
void build() {
for (int i = 1; i <= n; i ++) rk[i] = s[i];
for (int i = 1; i <= n; i ++) cnt[rk[i]] ++;
for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i --) sa[cnt[rk[i]] --] = i;
for (int k = 1, p = 0; k < n; k <<= 1, m = p) {
p = 0;
for (int i = n - k + 1; i <= n; i ++) id[++ p] = i;
for (int i = 1; i <= n; i ++)
if (sa[i] > k) id[++ p] = sa[i] - k;
for (int i = 0; i <= m; i ++) cnt[i] = 0;
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 (int i = n; i >= 1; i --) sa[cnt[px[i]] --] = id[i];
for (int i = 1; i <= n; i ++) tmp_rk[i] = rk[i];
p = 0;
for (int i = 1; i <= n; i ++) rk[sa[i]] = same(sa[i - 1], sa[i], k) ? p : ++ p;
}
for (int i = 1, h = 0; i <= n; i ++) {
if (h) h --;
while (s[i + h] == s[sa[rk[i] - 1] + h]) h ++;
height[rk[i]] = h;
}
}
}
树上 SA 求法:
- \(\mathcal{O}(n \log^2 n)\) 求树上 SA:sort + 树上倍增 + hash。当比较函数时间开销较大时,使用
std::stable_sort()
。 - \(\mathcal{O}(n \log n)\) 求树上 SA:倍增 + 双关键字排序。
// 边带权
int tot, head[N], ver[N * 2], edge[N * 2], Next[N * 2];
void add_edge(int u, int v, int w) {
ver[++ tot] = v; edge[tot] = w; Next[tot] = head[u]; head[u] = tot;
}
int anc[logN + 1][N];
int col[N];
void dfs(int u, int fu) {
anc[0][u] = fu;
for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i], w = edge[i];
if (v == fu) continue;
col[v] = w;
dfs(v, u);
}
}
namespace SA {
int m = 256;
int sa[N], rk[N], height[N];
int cnt[N], id[N], px[N];
int anc_rk[logN + 1][N];
int get_lcp(int x, int y) {
int cur = 0;
for (int i = logN; i >= 0; i --) {
if (!anc[i][x] || !anc[i][y]) continue;
if (anc_rk[i][y] ^ anc_rk[i][y]) continue;
x = anc[i][x], y = anc[i][y], cur ^= (1 << i);
}
return cur;
}
bool same(int x, int y, int k) {
int p = anc[k][x] ? anc_rk[k][anc[k][x]] : -1;
int q = anc[k][y] ? anc_rk[k][anc[k][y]] : -1;
return anc_rk[k][x] == anc_rk[k][y] && p == q;
}
void build() {
for (int i = 1; i <= n; i ++) rk[i] = col[i];
for (int i = 1; i <= n; i ++) cnt[rk[i]] ++;
for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i --) sa[cnt[rk[i]] --] = i;
for (int k = 0, p = 0; (1 << k) < n; k ++, m = p) {
for (int i = 0; i <= m; i ++) cnt[i] = 0;
for (int i = 1; i <= n; i ++) cnt[px[i] = rk[anc[k][i]]] ++;
for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i --) id[cnt[px[i]] --] = i;
for (int i = 0; i <= m; i ++) cnt[i] = 0;
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 (int i = n; i >= 1; i --) sa[cnt[px[i]] --] = id[i];
for (int i = 1; i <= n; i ++) anc_rk[k][i] = rk[i];
p = 0;
for (int i = 1; i <= n; i ++) rk[sa[i]] = same(sa[i - 1], sa[i], k) ? p : ++ p;
}
for (int i = 1; i <= n; i ++) rk[sa[i]] = i;
for (int i = 2; i <= n; i ++) height[i] = get_lcp(sa[i - 1], sa[i]);
}
}
// 点带权
int tot, head[N], ver[N * 2], edge[N * 2], Next[N * 2];
void add_edge(int u, int v, int w) {
ver[++ tot] = v; edge[tot] = w; Next[tot] = head[u]; head[u] = tot;
}
int dep[N];
int anc[logN + 1][N];
void dfs(int u, int fu) {
dep[u] = dep[fu] + 1;
anc[0][u] = fu;
for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == fu) continue;
dfs(v, u);
}
}
namespace SA {
int m = 256;
int sa[N], rk[N], height[N];
int cnt[N], id[N], px[N];
int anc_rk[logN + 1][N];
int get_lcp(int x, int y) {
int cur = 0;
for (int i = logN; i >= 0; i --) {
if (std::min(dep[x], dep[y]) < (1 << i)) continue;
if (anc_rk[i][x] ^ anc_rk[i][y]) continue;
x = anc[i][x], y = anc[i][y], cur ^= (1 << i);
}
return cur;
}
bool same(int x, int y, int k) {
int p = anc[k][x] ? anc_rk[k][anc[k][x]] : -1;
int q = anc[k][y] ? anc_rk[k][anc[k][y]] : -1;
return anc_rk[k][x] == anc_rk[k][y] && p == q;
}
void build() {
for (int i = 1; i <= n; i ++) rk[i] = s[i];
for (int i = 1; i <= n; i ++) cnt[rk[i]] ++;
for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i --) sa[cnt[rk[i]] --] = i;
for (int k = 0, p = 0; (1 << k) < n; k ++, m = p) {
for (int i = 0; i <= m; i ++) cnt[i] = 0;
for (int i = 1; i <= n; i ++) cnt[px[i] = rk[anc[k][i]]] ++;
for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i --) id[cnt[px[i]] --] = i;
for (int i = 0; i <= m; i ++) cnt[i] = 0;
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 (int i = n; i >= 1; i --) sa[cnt[px[i]] --] = id[i];
for (int i = 1; i <= n; i ++) anc_rk[k][i] = rk[i];
p = 0;
for (int i = 1; i <= n; i ++) rk[sa[i]] = same(sa[i - 1], sa[i], k) ? p : ++ p;
}
for (int i = 1; i <= n; i ++) rk[sa[i]] = i;
for (int i = 2; i <= n; i ++) height[i] = get_lcp(sa[i - 1], sa[i]);
}
}
本质不同子串个数:
SAM
SAM 与其他后缀数据结构的关系:
- 后缀 trie:一个字符串 \(s\) 的所有后缀组成的 trie。
- 后缀 trie 上建 AC 自动机的 fail 指针:表示删去某个状态所表示的串的首字符。
- 后缀树:后缀 trie 虚树化(仅保留所有后缀在后缀 trie 上的终止节点,及两两 LCA)。
- 后缀自动机的 parent 树:反串的后缀树。
SAM:SAM 是一个状态自动机,形态是 DAG。一个起点,若干终点。原串的所有子串,与 SAM 上从起点开始、任意点结束的所有路径一一对应、不重不漏。
SAM 中的边:SAM 主要考虑两种边
- 普通转移边:表示在某个状态所表示的串后面加一个字符。
- 后缀链接 link:表示将某个状态所表示的最短串的首字母删除。这类边构成一棵树,称作 parent 树。不一定满足 \(\mathrm{link}_i < i\)。
SAM 节点个数:\(\leq 2n - 1\)。
SAM 转移个数:\(\leq 3n - 4\)。
namespace SAM {
const int SIZE = N * 2;
int nClock = 1, Last = 1;
struct node {
int trans[26];
int link, maxl;
} t[SIZE];
void extend(int c) {
int p = Last,
np = Last = ++ nClock;
t[np].maxl = t[p].maxl + 1;
for (; p && t[p].trans[c] == 0; p = t[p].link) t[p].trans[c] = np;
if (!p) {
t[np].link = 1;
} else {
int q = t[p].trans[c];
if (t[q].maxl == t[p].maxl + 1) {
t[np].link = q;
} else {
int nq = ++ nClock; t[nq] = t[q]; t[nq].maxl = t[p].maxl + 1;
t[np].link = t[q].link = nq;
for (; p && t[p].trans[c] == q; p = t[p].link) t[p].trans[c] = nq;
}
}
}
}
- 建 SAM 时,当字符集很大时,需要用到
std::map
建 SAM。
namespace SAM {
const int SIZE = N * 2;
int nClock = 1, Last = 1;
struct node {
std::map<int, int> trans;
int link, maxl;
} t[SIZE];
void extend(int c) {
int p = Last,
np = Last = ++ nClock;
cnt[np] ++;
t[np].maxl = t[p].maxl + 1;
for (; p && t[p].trans[c] == 0; p = t[p].link) t[p].trans[c] = np;
if (!p) {
t[np].link = 1;
} else {
int q = t[p].trans[c];
if (t[q].maxl == t[p].maxl + 1) {
t[np].link = q;
} else {
int nq = ++ nClock; t[nq] = t[q]; t[nq].maxl = t[p].maxl + 1;
t[np].link = t[q].link = nq;
for (; p && t[p].trans[c] == q; p = t[p].link) t[p].trans[c] = nq;
}
}
}
}
\(\mathrm{endpos}(s)\):子串 \(s\) 的所有终止位置集合(right 集合)。
- SAM 中的每一个状态都对应一个 \(\mathrm{endpos}\) 等价类。
- SAM 中的每一个状态(\(\mathrm{endpos}\) 等价类),在其所包含的所有子串中,短串为长串的后缀,长度构成一个连续段。
- 若 \(s_1\) 为 \(s_2\) 的后缀,有 \(\mathrm{endpos}(s_1) \supseteq \mathrm{endpos}(s_2)\);否则,有 \(\mathrm{endpos}(s_1) \bigcap \mathrm{endpos}(s_2) = \varnothing\)。
\(\mathrm{endpos}(s)\) 的维护:在 parent 树上运用可持久化线段树合并维护。
namespace SGT {
const int pond = ...;
int nClock;
struct node {
int lc, rc;
} t[pond];
void insert(int &p, int l, int r, int x) {
p = ++ nClock;
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid)
insert(t[p].lc, l, mid, x);
else
insert(t[p].rc, mid + 1, r, x);
}
int merge(int p, int q) {
if (!p || !q) return p ^ q;
int u = ++ nClock;
t[u].lc = merge(t[p].lc, t[q].lc);
t[u].rc = merge(t[p].rc, t[q].rc);
return u;
}
bool exist(int p, int l, int r, int s, int e) {
if (!p) return 0;
if (s <= l && r <= e) return 1;
int mid = (l + r) >> 1;
if (s <= mid && exist(t[p].lc, l, mid, s, e)) return 1;
if (mid < e && exist(t[p].rc, mid + 1, r, s, e)) return 1;
return 0;
}
}
namespace SAM {
const int SIZE = N * 2;
int nClock = 1, Last = 1;
struct node {
int trans[26];
int link, maxl;
} t[SIZE];
int ed[N];
void extend(int c) {
int p = Last,
np = Last = ++ nClock;
t[np].maxl = t[p].maxl + 1;
ed[t[np].maxl] = np;
for (; p && t[p].trans[c] == 0; p = t[p].link) t[p].trans[c] = np;
if (!p) {
t[np].link = 1;
} else {
int q = t[p].trans[c];
if (t[q].maxl == t[p].maxl + 1) {
t[np].link = q;
} else {
int nq = ++ nClock, t[nq] = t[q], t[nq].maxl = t[p].maxl + 1;
t[np].link = t[q].link = nq;
for (; p && t[p].trans[c] == q; p = t[p].link) t[p].trans[c] = nq;
}
}
}
int root[SIZE];
int tot, head[SIZE], ver[SIZE], Next[SIZE];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
void dfs(int u) {
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
dfs(v);
root[u] = SGT::merge(root[u], root[v]);
}
}
void build_tree() {
for (int i = 2; i <= nClock; i ++) add_edge(t[i].link, i);
for (int i = 1; i <= n; i ++) SGT::insert(root[ed[i]], 1, n, i);
dfs(1);
}
}
广义 SAM:待填。
在某个状态所表示的串前面删一个字符:若当前串的长度等于当前状态的 \(\mathrm{minl}\),跳后缀链接即可。
在某个状态所表示的串前面加一个字符:维护每个状态 \(u\) 的 \(\mathrm{endpos}\) 集合中任意一个元素 \(\mathrm{pos}_u\),即可在原串中定位状态 \(u\)。
- 若当前串的长度等于当前状态的 \(\mathrm{maxl}\),则相当于要在 parent 树上向一个儿子节点走。在建 parent 树的时候,预处理每一个状态前加一个字符,能走到哪个儿子即可(枚举 \(u\) 与其每个儿子 \(v\),在原串中定位即得从 \(u\) 到 \(v\) 需加什么字符)。
- 若当前串的长度小于当前状态的 \(\mathrm{maxl}\),则相当于要考虑当前状态是否可以容纳新串,只需判断新加的字符是否与原串对应位置上的字符匹配即可。
子串定位(求 \(s[l : r]\) 在 SAM 中的对应状态):先定位 \(s[1 : r]\) 在 SAM 中的对应状态(在构造 SAM 的过程中记录即可),在 parent 树上倍增,找到 \(\mathrm{maxl}\) 大于等于 \(r - l + 1\) 且深度最浅的状态。
子串匹配(给定模式串 \(S\) 与文本串 \(T\),对每个 \(r\) 求最小的 \(l\) 使得 \(T[l : r]\) 为 \(S\) 的子串):采用增量法。维护当前匹配到的子串长度与在 SAM 上的对应状态,当右端点扩展时,不断跳后缀链接,直到跳到起点或存在相应的转移边为止,若存在相应的转移边,则走该相应的转移边。
子 SAM(给定模式串 \(S\) 与文本串 \(T\),令 \(T\) 在 \(S[l : r]\) 中匹配):判断由 \(S[l : r]\) 组成的 SAM 是否存在相应的转移边,首先在原 SAM 中就需要存在相应的转移边,其次在原 SAM 中新状态的 \(\mathrm{endpos}\) 集合需要在 \([l + \mathrm{len}’ - 1, r]\) 中有元素。失配时,应尝试 \(\mathrm{len}’ \gets \mathrm{len}’ - 1\) 后继续查询,而非直接跳失配指针。
PAM
PAM:PAM 是一个状态自动机,形态是由两棵树构成的森林。有两个起点(偶根 \(0\),奇根 \(1\)),若干终点。原串的所有回文子串,与 PAM 上的所有状态对应。
PAM 中的边:PAM 主要考虑两种边:
- 普通转移边:表示在某个状态所表示的串前后各加同一个字符。
- 后缀链接 link:表示某个状态所表示的串的最长回文后缀。这类边构成一棵树,称作 parent 树。
PAM 节点个数(本质不同回文子串个数):\(\leq n\)。
- 在任意一个串后面加一个字符,运用反证法可知新增的本质不同回文子串个数至多 \(+1\)。
namespace PAM {
int cur_len, str[N];
int nClock, Last;
struct node {
int trans[26];
int link, len;
} t[N];
void init() {
t[0].len = 0, t[0].link = 1;
t[1].len = -1;
str[cur_len = 0] = -1;
nClock = 1, Last = 1;
}
int find(int p) {
while (str[cur_len - t[p].len - 1] != str[cur_len]) p = t[p].link;
return p;
}
void extend(int c) {
str[++ cur_len] = c;
int p = find(Last);
if (!t[p].trans[c]) {
int np = ++ nClock;
t[np].len = t[p].len + 2;
t[np].link = t[find(t[p].link)].trans[c];
t[p].trans[c] = np;
}
Last = t[p].trans[c];
}
}
PAM 的 half 指针:表示某个状态所表示的串,长度小于等于该串一半的最长回文后缀。
namespace PAM {
int cur_len, str[N];
int nClock, Last;
struct node {
int trans[26];
int link, half, len;
} t[N];
void init() {
t[0].len = 0, t[0].link = 1;
t[1].len = -1;
str[cur_len = 0] = -1;
nClock = 1, Last = 1;
}
int find(int p) {
while (str[cur_len - t[p].len - 1] != str[cur_len]) p = t[p].link;
return p;
}
int find_half(int p, int L) {
while (t[p].len + 2 > L || str[cur_len - t[p].len - 1] != str[cur_len]) p = t[p].link;
return p;
}
void extend(int c) {
str[++ cur_len] = c;
int p = find(Last);
if (!t[p].trans[c]) {
int np = ++ nClock;
t[np].len = t[p].len + 2;
t[np].link = t[find(t[p].link)].trans[c];
t[np].half = t[np].len <= 2 ? t[np].link : t[find_half(t[p].half, t[np].len / 2)].trans[c];
t[p].trans[c] = np;
}
Last = t[p].trans[c];
}
}