for(;p && !sam[p].nex[x];p = sam[p].fa) sam[p].nex[x] = now:
在 p 的基础上加上一个字符以后只会对原串的所有后缀的 endpos 产生影响,根据母树定义,父子之间以及一个类的子串之间都是后缀包含关系,加上 ch 就相当于让 p 及其祖先都进行了一次 ch 的转移,现在顺着每个节点跳 fa 就可以看成 遍历 longest(p) 的所有后缀串(其实是压缩遍历),直到找到一个曾经接受过 ch 的状态,同时让这条路径上每一个类都向 now 进行转移(毕竟加了 ch, p 这个类上的串以及他的所有祖先( p 的子串)就都可以向 now 转移)
循环条件 p为真 是防止跳出根节点。
Case 1:
if(!p) sam[now].fa = 1;
当循环结束时如果 p 为假则说明跳到了根,这个情况只有在加入了并未在前面出现的新字符时才会出现,因为没有任何一个状态接受过 ch,这是它的祖先只能是1,所以令 \text{fa}(now) = 1
以下解释是 abcbc 。
加入一个 a 以后长这样(虚线表示父亲,黑线表示转移边,下划线表示这个类的串)
加入一个 b,新建3节点, p指针顺着2跳到了根节点同时让2向3转移,跳到了根节点,说明 b 是个新字符,从根节点向3转移一次,同时让 \text{fa}(2) = 1)
给定一个只包含小写字母的字符串 S。
请你求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。
用 siz_u 表示节点 u 的节点大小,重建母树然后搜索,统计每个节点的 siz 然后乘上该节点的 len 取 max 即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
string s;
int head[N << 1];
long long siz[N << 1];
struct edge {
int nex, to;
} e[N << 1];
void add(int u, int v) {
static int tot = 0;
e[++tot].to = v;
e[tot].nex = head[u];
head[u] = tot;
}
int tot = 1, last = 1;
struct SAM {
int fa, len, nex[26];
} sam[N << 1];
void insert(int x) {
int now = ++tot, p = last;
siz[now] = 1;
sam[now].len = sam[last].len + 1;
for(; p && !sam[p].nex[x]; p = sam[p].fa)
sam[p].nex[x] = now;
if(!p)
sam[now].fa = 1;
else {
int q = sam[p].nex[x];
if(sam[q].len == sam[p].len + 1) sam[now].fa = q;
else {
int tmp = ++tot;
sam[tmp] = sam[q];
sam[now].fa = sam[q].fa = tmp;
sam[tmp].len = sam[p].len + 1;
for(; p && sam[p].nex[x] == q; p = sam[p].fa) sam[p].nex[x] = tmp;
}
}
last = now;
}
void dfs(int now) {
for(int i = head[now]; i; i = e[i].nex) {
int to = e[i].to;
dfs(to);
siz[now] = siz[now] + siz[to];
}
}
void find() {
long long ans = 0;
for(int i = 1;i <= tot; ++i)
if(siz[i] != 1)
ans = max(siz[i] * sam[i].len, ans);
printf("%lld", ans);
}
int main() {
cin >> s;
for(int i = 0, len = s.size(); i < len; ++i) insert(s[i] - 'a');
for(int i = 2; i <= tot; ++i) add(sam[i].fa, i);
dfs(1);
find();
return 0;
}
P3809 【模板】后缀排序
输出每个后缀串按字典序升序排序后的排名
没错SAM还能求SA,根据定义, 跑完 SAM 以后节点与节点之间的 fa 能构成一颗 parent\ tree, 这棵树上叶子节点的 longest 就是原串的前缀串,所以只要建好 SAM 后按字典序重建母树,然后从根节点往下深搜到达每一个叶子节点即可......个屁啊,事实证明对SAM最大的制约就是空间,这题空间太小了,如果你愿意的话可以参考题解区大佬的挂链哈希SAM。
P2408 不同子串个数
求给定的字符串的本质不同子串个数(即两个子串相同但处于不同位置算作一个子串)
考虑DP,SAM上每一段路径都是一个子串,设 f_v 为节点 v 开始的路径数,可得如下方程:
f_u = 1 + \sum\limits_{e(u,v)\in DAG}f_v
最后输出 f_1 - 1 即可,因为要跑出空串。
时间复杂度 O(|S|)。
long long dfs(int now) {
if(ans[now]) return ans[now];
for(int i = 0;i < 26; ++i)
if(sam[now].nex[i])
ans[now] += dfs(sam[now].nex[i]) + 1;
return ans[now];
}
int main() {
scanf("%d\n", &n);
for(int i = 1;i <= n; ++i) add(getchar() - 'a');
printf("%lld", dfs(1));
return 0;
}
for(int i = 1, p = 1;i <= n;++i ) {//往下走n步
auto it = sam[p].nex.begin();//遍历每一个点的出边
p = it -> second;//second 指向节点
printf("%d ", it -> first);//first表示边上的值;
}
P3975 [TJOI2015]弦论
$t = 1$ 求所有子串中的第 $k$ 小子串
还是 siz_i 表示 endpos_i 的大小。
t = 0$ 则初始化 $siz[i] = 1
设 $sum_i$ 表示有 $sum_i$ 个子串经过 $i$ 号节点,统计之后搜索,如果该节点为 $u$, 其转移节点为 $v$。
若 $sum_v \lt k$ 则将其减去。
若 $sum_v \geq k$ 则搜索进入 $(v, k)$,同时输出该转移边上的字符。
```cpp
#include <bits/stdc++.h>
#define N 500010
using namespace std;
long long k;
int t, len, cnt[N << 1], order[N << 1], siz[N << 1], sum[N << 1];
char s[N];
int tot = 1, last = 1;
struct SAM {
int fa, len, nex[26];
}sam[N << 1];
void add(int x) {
int now = ++tot, p = last;
siz[now] = 1;
sam[now].len = sam[p].len + 1;
for(;p && !sam[p].nex[x]; p = sam[p].fa) sam[p].nex[x] = now;
if(!p) sam[now].fa = 1;
else {
int q = sam[p].nex[x];
if(sam[q].len == sam[p].len + 1) sam[now].fa = q;
else{
int tmp = ++tot;
sam[tmp] = sam[q];
sam[now].fa = sam[q].fa = tmp;
sam[tmp].len = sam[p].len + 1;
for(; p && sam[p].nex[x] == q; p = sam[p].fa) sam[p].nex[x] = tmp;//让p类的所有祖先都向新扣下来的节点转移
}
}
last = now;
}
void dfs(int p, int k) {
if(k <= siz[p]) return;
k -= siz[p];
for(int i = 0;i < 26; ++i) {
int to = sam[p].nex[i];
if(!to) continue;
if(k > sum[to]) {
k -= sum[to];
}else {
putchar(i + 'a'), dfs(to, k);
return;
}
}
}
int main() {
scanf("%s", s);len = strlen(s);
scanf("%d %lld", &t, &k);
for(int i = 0;i < len; ++i) add(s[i] - 'a');
for(int i = 1;i <= tot; ++i) ++cnt[sam[i].len];
for(int i = 1;i <= len; ++i) cnt[i] += cnt[i - 1];
for(int i = 1;i <= tot; ++i) order[cnt[sam[i].len]--] = i;//倒拓扑序
for(int i = tot;i >= 1; --i) siz[sam[order[i]].fa] += siz[order[i]];
for(int i = 1;i <= tot; ++i) t==0?(sum[i] = siz[i] = 1):(sum[i] = siz[i]);
siz[1] = sum[1] = 0;
for(int i = tot;i >= 1;--i)
for(int j = 0;j < 26; ++j)
sum[order[i]] += sum[sam[order[i]].nex[j]];
if(sum[1] >= k) dfs(1, k);
else printf("-1");
return 0;
}
```
[SP1811 LCS - Longest Common Substring](https://www.luogu.com.cn/problem/SP1811)
>求所给两个串的最长公共子串
对第一个串建立SAM,把第二个串放在SAM从根节点开始跑,如果该节点可以转移这个字符那就转移,如果没有这个转移边就跳 $fa$, 直到可以转移,为什么这么做是对的呢,在SAM中转移边相当于在串的后面添加字符,母树上的父子之间就相当于在一个串的前面增添字符,从儿子到父亲就是从前面删除字符,转移就相当于从后面删除字符,故这样做是对的。拿一个变量记录最长的匹配长度即可。
```cpp
void find() {
int ans = 0, now = 0;
for(int i = 0, p = 1, len = strlen(s);i < len; ++i) {
while(p && !sam[p].nex[s[i]-'a']) {
p = sam[p].fa;
now = sam[p].len;
}
if(!p) p = 1;
if(sam[p].nex[s[i] - 'a']) {
p = sam[p].nex[s[i] - 'a'];
++now;
ans = max(now, ans);
}
}
printf("%d", ans);
}
```
[P5546 [POI2000]公共串](https://www.luogu.com.cn/problem/P5546)
一样的题,不过变成了多串,拿最短的串建自动机(直接拿第一个建也无所谓),把其他串放在上面跑,方法和双串问题一样,但是每个节点要对每个串的最长匹配长度取 min,跑完之后在所有 min 中取 max 即为答案。
```cpp
#include <bits/stdc++.h>
#define N 2010
using namespace std;
char s[N];
int n, len, ans;
int mx[N << 1], mn[N << 1], order[N << 1], cnt[N];
int tot = 1, last = 1;
struct SAM {
int fa, len, nex[26];
}sam[N << 1];
void add(int x) {
int now = ++tot, p = last;
sam[now].len = sam[p].len + 1;
for(;p && !sam[p].nex[x]; p = sam[p].fa) sam[p].nex[x] = now;
if(!p) sam[now].fa = 1;
else {
int q = sam[p].nex[x];
if(sam[q].len == sam[p].len + 1) sam[now].fa = q;
else{
int tmp = ++tot;
sam[tmp] = sam[q];
sam[now].fa = sam[q].fa = tmp;
sam[tmp].len = sam[p].len + 1;
for(; p && sam[p].nex[x] == q; p = sam[p].fa) sam[p].nex[x] = tmp;
}
}
last = now;
}
void toposort(int n) {
for(int i = 1;i <= tot; ++i) ++cnt[sam[i].len], mn[i] = sam[i].len;
for(int i = 1;i <= n; ++i) cnt[i] += cnt[i - 1];
for(int i = 1;i <= tot; ++i) order[cnt[sam[i].len]--] = i;
}
void find(char* s, int n) {
int p = 1,l = 0;
for(int i = 0, num;i < n; ++i) {
num = s[i] - 'a';
if(sam[p].nex[num]) p = sam[p].nex[num], mx[p] = max(mx[p], ++l);
else {
for(;p&&!sam[p].nex[num];) p = sam[p].fa;
if(!p) p = 1, l = 0;
else l = sam[p].len + 1, p = sam[p].nex[num], mx[p] = max(mx[p], l);
}
}
for(int i = tot, now, fa; i >= 1; --i) {
now = order[i];
fa = sam[order[i]].fa;
mx[fa] = max(mx[fa], min(mx[now], sam[fa].len));
mn[now] = min(mn[now], mx[now]);
mx[now] = 0;
}
}
int main() {
scanf("%d\n", &n);
scanf("%s", s);len = strlen(s);
for(int i = 0;i < len; ++i) add(s[i] - 'a');
toposort(len);
for(int i = 1;i < n; ++i) {
scanf("%s", s);
find(s, strlen(s));
}
for(int i = 1;i <= tot; ++i) ans = max(ans, mn[i]);
printf("%d", ans);
return 0;
}
```
[P4248 [AHOI2013]差异](https://www.luogu.com.cn/problem/P4248)
>给定一个长度为 $n$ 的字符串 $S$,令 $T_i$ 表示它从第 $i$ 个字符开始的后缀。求
$$\displaystyle \sum_{1\leqslant i<j\leqslant n}\text{len}(T_i)+\text{len}(T_j)-2\times\text{lcp}(T_i,T_j)$$
拆分一下
$$\displaystyle \sum_{1\leqslant i<j\leqslant n}\text{len}(T_i)+\text{len}(T_j)-\sum_{1\leqslant i<j\leqslant n}2\times\text{lcp}(T_i,T_j)$$
前面相当于一个常数
$$\sum_{i=1}^{n-1}\sum_{j=i+1}^ni+j=(n-1)\times\sum_{i=1}^ni=\frac{n(n-1)(n+1)}2$$
原式就变成了
$$\displaystyle \frac{n(n-1)(n+1)}{2}-\sum_{1\leqslant i<j\leqslant n}2\times\text{lcp}(T_i,T_j)$$
母树的叶子节点是原串的前缀串,其 LCA 就是其 LCS ,把原串倒置插入 SAM ,叶子结点就变成了原串的后缀串,其 LCA 就变成了其 LCP,其实就是把求后缀串的 LCP 转化为了求前缀串的 LCS。现在其实就是求每一个点作为他儿子的 LCA 的贡献。
拓扑排序一下,倒序枚举,对于一个节点 $now$, 它的父亲 $fa$ 已经统计了一部分儿子的信息且这些儿子和 $now$ 没有任何关联(之前统计的是另一部分的儿子), $now$ 的儿子可以和已经统计的 $fa$ 的儿子两两配对, $fa$ 就是配对以后的 LCA,$siz[now] * siz[fa]$ 就是配对出来的点对个数,然后在乘上 $\text{len}(fa)$ 即可,所以统计答案 `ans += siz[now] * siz[fa] * len[fa];` 即可。
```cpp
#include <bits/stdc++.h>
#define LL long long
#define N 500010
using namespace std;
int n;
char s[N];
LL ans, siz[N << 1];
int cnt[N], order[N << 1];
int tot = 1, last = 1;
struct SAM {
int fa, len, nex[26];
}sam[N << 1];
void add(int x) {
int now = ++tot, p = last;
sam[now].len = sam[p].len + 1;
siz[now] = 1;
for(;p&&!sam[p].nex[x];p = sam[p].fa) sam[p].nex[x] = now;
if(!p) sam[now].fa = 1;
else {
int q = sam[p].nex[x];
if(sam[q].len == sam[p].len + 1) sam[now].fa = q;
else {
int tmp = ++tot;
sam[tmp] = sam[q];
sam[q].fa = sam[now].fa = tmp;
sam[tmp].len = sam[p].len + 1;
for(;p&&sam[p].nex[x] == q; p = sam[p].fa) sam[p].nex[x] = tmp;
}
}
last = now;
}
void work() {
n = strlen(s);
for(int i = n - 1;i >= 0; --i) add(s[i] - 'a');
for(int i = 1;i <= tot; ++i) ++cnt[sam[i].len];
for(int i = 1;i <= n; ++i) cnt[i] += cnt[i - 1];
for(int i = 1;i <= tot; ++i) order[cnt[sam[i].len]--] = i;
for(int i = tot, now;i >= 1; --i) {
now = order[i];
ans += (siz[now] * siz[sam[now].fa]) * sam[sam[now].fa].len;
siz[sam[now].fa] += siz[now];
}
printf("%lld", ((LL)n * (LL)(n - 1)) / 2 * (LL)(n + 1) - 2 * ans);
}
int main() {
scanf("%s", s);
work();
return 0;
}
```
# 待建设项目
## 广义SAM
## NOI原题
- [P4770 [NOI2018] 你的名字](https://www.luogu.com.cn/problem/P4770)
- [P2178 [NOI2015] 品酒大会](https://www.luogu.com.cn/problem/P2178)
参考资料(排名不分先后)
《算法竞赛》 —— 罗勇军,郭卫斌
<https://zhuanlan.zhihu.com/p/410131141>
<https://www.luogu.com.cn/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie>
[《后缀自动机》—— 陈立杰](https://wenku.baidu.com/view/b3e54c05ed3a87c24028915f804d2b160a4e8639.html_wkts_=1684331267951&bdQuery=%E9%99%88%E7%AB%8B%E6%9D%B0+%E5%90%8E%E7%BC%80%E8%87%AA%E5%8A%A8%E6%9C%BA+%E7%99%BE%E5%BA%A6%E6%96%87%E5%BA%93)
本文完