标签:子串 nxt SAM fa 后缀 int 习记 maxn
前言
本文没有构造证明,因为我不会
基础概念看看就好,自娱自乐。
后期重点更新相关题目的简单总结,方便复习
SAM
基础概念
S 的后缀自动机是一种能够识别所有 S 的子串的自动机类型的数据结构(DFA)。
暴力后缀自动机
对于字符串 \(S\),建立插入了 \(|S|\) 个后缀的 Trie 树。这样显然可以查到所有的子串,但时空复杂度都为 \(O(n^2)\) ,显然不可取,考虑如何优化
最简状态后缀自动机
给出结论:状态数 \(O(2*n)\),转移数 \(O(3*n)\)
定义
- \(s(w)\) 表示子串 w 对应的后缀自动机上的状态。
- \(trans(s, ch)\) 表示当前状态是 s,接收新字符 ch 之后到达的状态。
- \(Trans(s,str)\) 表示当前状态是 s, 接收新字符串 str 之后到达的状态。
- \(Suf[i]\) 表示从 i 位置开始的后缀,即 \(S[i, |S|]\)。
- 在后缀自动机中,一个状态表示的是结束位置相同,但长度不同的一系列串。
我们定义 \(Right(s)={r_1,r_2,...,r_m}\) ,表示 \(s\) 状态代表子串的出现位置右端点。
根据定义简单推论
- 在最简状态后缀自动机中,所有节点的 Right 集合互不相同
- 每个节点代表的串之间形成后缀关系,即所有串都是最长串的后缀。
- 每个节点代表的串的长度是连续区间,记为 [MinL(s), MaxL(s)]。
- 考虑同在 \(Right(s)\) 位置的长度为 \(MinL(s) - 1\) 的子串,设其所在的状态为 \(s′\),则必有 \(Right(s)\) 是 \(Right(s′)\) 的真子集
Right 性质
设两个状态 \(s\) 和 \(s′\),其 \(Right\) 集合分别为 \(R(s)\) 和 \(R(s′)\)。
假设 \(R(s) ∩ R(s′) = ∅\),并且 \(r ∈ R(s) ∩ R(s′)\)。
由于任何两个状态代表的串互不相同,不失一般性,可以认为 \(s\) 代表的
串都比 \(s′\) 代表的串长。
因此必然有 \(R(s) ⫋ R(s′)\)。
结论:两个不同状态的 Right 集合只存在两种关系:不相交,或者真包含。即形成一种树形结构。
SC(suffix-chain) 树(后缀链接树)
Parent 树、link 树
由 \(Right\) 集合的包含关系形成的树。
\(f(s)\) 表示状态 \(s\) 对应的 \(Right(s)\) 在 \(SC\) 树上的父节点。
SC树性质
- 每个前缀所在的状态两两不同。
- 共有 \(|S|\) 个叶子节点,分别对应于每个前缀 \(S[1, i]\) 所在的状态。
- 后缀链接树至多有 2|S| − 1 个节点,即至多有这么多不同的 Right 集合,
即后缀自动机节点个数为 \(O(2n)\)。 - 任意串 w 的后缀全部位于 s(w) 的后缀链接路径上。
- 若某个状态 s 拥有 ch 的转移边,那么 f[s] 也一定有 ch 的转移边。(但
不一定转移到同一个状态) - 每个状态 s 的 Right(s) 等价于他在后缀链接树子树的叶子节点集合。
一些显然的观察
- 对于任意状态 s,\(MaxL[f(s)]\) = \(MinL[s]\) - 1。
- 因此每个状态只需要额外记录 \(L[s] = MaxL[s]\) 以及 f(s),于是状态 s 能
- 表示长度为 \((L[f(s)], L[s]]\) 的串。
- 所有终止状态都能代表至少一个后缀。
- Right 集合概念等价于子树概念,因此不需要额外维护
结点数为 \(O(2*n)\)
转移数为 \(O(3*n)\)。\(|2S-1|\) 树边 + \(S\) 条非树边,每个后缀对应一条非树边。
小结
- 状态个数以及转移边个数都是线性。
- 每个状态 s 能够表示以任意 R(s) 中的位置作为右端点的,长度范围
在 (L[f(s)], L[s]] 的子串。每个状态可以看作是一个 Right 意义上的等价
类。\(Mxlen[f(s)] = Minlen[s] - 1\) - 在构造过程中,需要维护每个状态的 f(s),L[s],以及
trans(s, ch)。(Right 集合不需要真的去维护,转化为子树查询即可。)
模板
const int maxn = 1e6 + 10;
struct SAM {
//basic
const char* s;
int last, cnt, len;
int nxt[maxn * 2][26],fa[maxn * 2],l[maxn * 2];
//extension
int cntA[maxn * 2], id[maxn * 2];/*辅助拓扑更新*/
int num[maxn * 2];/*每个节点代表的所有串的出现次数*/
SAM () { clear(); }
void clear() {
last = cnt = 1, l[1] = fa[1] = 0, memset(nxt[1], 0, sizeof nxt[1]);
}
void init(const char * str, int _len) {
s = str, len = _len;
for (int i = 1; i <= _len; i++)
extend(str[i] - 'a');
}
void extend(int c) {
int p = last, np = ++cnt;
memset(nxt[cnt], 0, sizeof nxt[cnt]);
l[np] = l[p] + 1, last = np;
while (p && !nxt[p][c]) nxt[p][c] = np, p = fa[p];
if (!p) fa[np] = 1;
else {
int q = nxt[p][c];
if (l[q] == l[p] + 1) fa[np] = q;
else {
int nq = ++cnt;
l[nq] = l[p] + 1;
memcpy(nxt[nq], nxt[q], sizeof(nxt[q]));
fa[nq] = fa[q], fa[np] = fa[q] = nq;
while (nxt[p][c] == q) nxt[p][c] = nq, p = fa[p];
}
}
}
void build() {
memset(cntA, 0, sizeof cntA);
memset(num, 0, sizeof num);
for (int i = 1; i <= cnt; i++) cntA[l[i]]++;
for (int i = 1; i <= cnt; i++) cntA[i] += cntA[i - 1];
for (int i = cnt; i >= 1; i--) id[cntA[l[i]]--] = i;
/*更新主串节点*/
int temp = 1;
for (int i = 1; i <= len; i++) {
num[temp = nxt[temp][s[i] - 'a']] = 1;
}
/*拓扑更新*/
for (int i = cnt; i >= 1; i--) {
// basic
int x = id[i];
num[fa[x]] += num[x];
// extension
}
// extension
}
void debug(){
for (int i = cnt; i >= 1; i--){
printf("num[%d]=%d l[%d]=%d fa[%d]=%d\n",i,num[i],i,l[i],i,fa[i]);
}
}
}sam;
应用
SAM上拓扑序与 DFS
求长度为 \(x\) 的子串最大出现次数
- 拓扑更新时,更新每个点对应最大长度的最大值,打上标记。
- 拓扑更新结束,从后往前推标记即可,保证是按照拓扑序更新。
求子串 [l, r] 的出现次数
- 子串 [l, r] 一定是前缀 [1, r] 的后缀,找到前缀的定位然后倍增跳到 root 的后缀链接路径。
- 找到深度最小的满足
l[x] >= r - l + 1
的点,返回它的 \(|Right|\) 集合大小就是子串的出现次数。
求最长公共子串
- 求 \(S,T\) 最长公共子串,对 \(S\) 建立 SAM
- 将 T 在 SAM 上进行匹配,对于匹配到的每个节点,其到根后缀链接路径的点也能匹配目前的长度。
- 对每个状态点打上最大值标记,然后 拓扑序 从后往前更新即可,最后记录最大值为最长公共子串
多模式求最长公共子串
- 为了防止被卡复杂度,对 最长的串 建立 SAM
- 将其他串各自在SAM上运行,各自对所有匹配到的节点取 min。
- 最后取所有节点的全局最大值就是最长公共子串。
求单串本质不同子串个数
- 答案为 \(\Sigma l[u] -l[fa[u]]\)
- 每个节点记录了对应 right 集合,不同长度的子串,两节点不同,要么前者属于后者后缀,要么不相交, 在同链上一定是前后缀关系。
- 因此将每个节点所记录的长度区间累加就是答案。
本质不同公共子串的个数(本质不同子串的交)
- 先求最长公共子串的长度
- 再类似上者,求本质不同子串的个数,记得要和 \(0\) 取 max
本质不同子串的并集大小。
- 利用容斥答案 = S 的本质不同子串个数 + T 的本质不同子串个数 - S和T本质不同子串的交。
求子串第 \(K\) 小
- 本质不同子串第 \(K\) 小
- 记录每个节点内的路径个数(与出现次数不同),拓扑序倒序dp计数即可。
- 类似主席树求第 \(k\) 小一样的做法即可,当
sz[ne] >= k
时,k --, now = ne, break;
- 证明,SAM内每条路径对应的都是一条不同的子串。
- 记录重复子串第 \(K\) 小
- 路径转移:
sz[u] = num[u], sz[u] += sz[v]
,初始由 1 变成num[u]
出现次数 - 询问时,
sz[ne] >= k
执行k -= num[ne]
- 路径转移:
link 树上问题
求子串 [l, r] 在子串 [L, R] 的出现次数
- 即询问 \(L+r-l\leq Right(S[l,r])\leq R\) 的个数
- 对 link 树 dfs 序建立主席树,查询 \(root[dfn[x] - 1]\) 和 \(root[dfn[x] + sz[x] - 1]\) 在值域 \([L+r-l,R]\) 的个数。
求 所有后缀长度加和 - 2 * 所有后缀 \(lcp\) 之和
- 往树上两点路径,树上差分上想,对字符串的反串建 SAM,这样两节点间的 lca 由 最长公共后缀 转变等价于 最长公共前缀
- 对反串后的前缀节点(实际对应原串后缀)
sz[np] = 1
,然后拓扑序更新。 - 记录每条边的边权为
l[p]-l[fa[p]]
,答案就是树上所有路径之和。 - 按边考虑贡献:
- 每条边对于所有路径会被经过
(n-sz[x])*sz[x]
次,乘上边权在倒序拓扑时直接统计即可。
- 每条边对于所有路径会被经过
求两串中各取出一个子串使得这两个子串相同的方案数
- 对一个串建立SAM,记录每个节点的前缀和
pre[u] = pre[fa[u]] + l[fa[u]] - l[fa[fa[u]]
- 将另一个串放在SAM上跑,每次答案加上
pre[now] + num[now] * (len - l[fa[now]])
即可。
关于广义SAM
在含有多个串信息的 Trie 上建立SAM。有离线和在线两种构造方法,直接给出模板
广义SAM离线模板
const int maxn = 1e6 + 10;
struct Trie {
int idx, fa[maxn], son[maxn][26], c[maxn];
Trie() {idx = 1;}
void insert(const char* s) {
int p = 1;
for (int i = 1; s[i]; i++) {
int u = s[i] - 'a';
if (!son[p][u]) son[p][u] = ++idx, fa[idx] = p, c[idx] = u;
p = son[p][u];
}
}
}Tr;
struct SAM {
//basic
const char* s;
int cnt, len;
int nxt[maxn * 2][26],fa[maxn * 2],l[maxn * 2];
queue<int> q;
//extension
int cntA[maxn * 2], id[maxn * 2];/*辅助拓扑更新*/
int num[maxn * 2];/*每个节点代表的所有串的出现次数*/
int pos[maxn * 2]; // Trie 上节点在 SAM 上对应的节点编号
SAM () { clear(); }
void clear() {
cnt = 1, l[1] = fa[1] = 0, memset(nxt[1], 0, sizeof nxt[1]);
}
void init() {
for (int i = 0; i < 26; i++) if (Tr.son[1][i]) q.push(Tr.son[1][i]);
pos[1] = 1;
while (!q.empty()) {
int t = q.front(); q.pop();
pos[t] = extend(Tr.c[t], pos[Tr.fa[t]]);
for (int i = 0; i < 26; i++) if (Tr.son[t][i]) q.push(Tr.son[t][i]);
}
}
int extend(int c, int last) {
int p = last, np = ++cnt;
memset(nxt[cnt], 0, sizeof nxt[cnt]);
l[np] = l[p] + 1, last = np;
while (p && !nxt[p][c]) nxt[p][c] = np, p = fa[p];
if (!p) fa[np] = 1;
else {
int q = nxt[p][c];
if (l[q] == l[p] + 1) fa[np] = q;
else {
int nq = ++cnt;
l[nq] = l[p] + 1;
memcpy(nxt[nq], nxt[q], sizeof(nxt[q]));
fa[nq] = fa[q], fa[np] = fa[q] = nq;
while (nxt[p][c] == q) nxt[p][c] = nq, p = fa[p];
}
}
return np;
}
void build() {
memset(cntA, 0, sizeof cntA);
memset(num, 0, sizeof num);
for (int i = 1; i <= cnt; i++) cntA[l[i]]++;
for (int i = 1; i <= cnt; i++) cntA[i] += cntA[i - 1];
for (int i = cnt; i >= 1; i--) id[cntA[l[i]]--] = i;
/*更新主串节点*/
int temp = 1;
for (int i = 1; i <= len; i++) {
num[temp = nxt[temp][s[i] - 'a']] = 1;
}
/*拓扑更新*/
for (int i = cnt; i >= 1; i--) {
// basic
int x = id[i];
num[fa[x]] += num[x];
// extension
}
// extension
}
void debug(){
for (int i = cnt; i >= 1; i--){
printf("num[%d]=%d l[%d]=%d fa[%d]=%d\n",i,num[i],i,l[i],i,fa[i]);
}
}
}exsam;
广义SAM在线模板
const int maxn = 2e6 + 10;
struct EXSAM {
//basic
const char* s;
int cnt, len;
int nxt[maxn * 2][26],fa[maxn * 2],l[maxn * 2];
//extension
queue<int> q;
int cntA[maxn * 2], id[maxn * 2];/*辅助拓扑更新*/
int num[maxn * 2];/*每个节点代表的所有串的出现次数*/
EXSAM () { clear(); }
void clear() {
cnt = 1, l[1] = fa[1] = 0, memset(nxt[1], 0, sizeof nxt[1]), memset(num, 0, sizeof num);
}
int extend(int c, int last, int idx = 0) {
if (nxt[last][c]) {
int p = last, x = nxt[p][c];
if (l[p] + 1 == l[x]) { num[x] = 1; return x;}
int y = ++cnt;
l[y] = l[p] + 1;
memcpy(nxt[y], nxt[x], sizeof nxt[x]);
while (p && nxt[p][c] == x) nxt[p][c] = y, p = fa[p];
fa[y] = fa[x], fa[x] = y;
num[y] = 1;
return y;
}
int p = last, np = ++cnt;
memset(nxt[cnt], 0, sizeof nxt[cnt]);
l[np] = l[p] + 1, last = np;
while (p && !nxt[p][c]) nxt[p][c] = np, p = fa[p];
if (!p) fa[np] = 1;
else {
int q = nxt[p][c];
if (l[q] == l[p] + 1) fa[np] = q;
else {
int nq = ++cnt;
l[nq] = l[p] + 1;
memcpy(nxt[nq], nxt[q], sizeof(nxt[q]));
fa[nq] = fa[q], fa[np] = fa[q] = nq;
while (nxt[p][c] == q) nxt[p][c] = nq, p = fa[p];
}
}
num[np] = 1;
return np;
}
void build() {
memset(cntA, 0, sizeof cntA);
for (int i = 2; i <= cnt; i++) ++ cntA[fa[i]];
for (int i = 1; i <= cnt; i++) if (!cntA[i]) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < 2; i++)
num[fa[u]] += num[u];
if (!(--cntA[fa[u]])) q.push(fa[u]);
}
}
}exsam;
广义SAM 应用
树上本质不同路径数
题意
给出一颗叶子结点不超过 \(20\) 个的无根树,每个节点上都有一个不超过 \(10\) 的数字,求树上本质不同的路径个数(两条路径相同定义为:其路径上所有节点上的数字依次相连组成的字符串相同)
思路
- 重要结论:一颗无根树上任意一条路径必定可以在以某个叶节点为根时,变成一条从上到下的路径(利于广义 SAM 的使用)。
参考资料
标签:子串,nxt,SAM,fa,后缀,int,习记,maxn
来源: https://www.cnblogs.com/Roshin/p/SAM_notes.html
本站声明:
1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。