后缀自动机 SAM
重要性质
- 后缀自动机的节点数不超过 。
以下是字符串 形成的图
模版讲解
- 指向的节点可以这么描述:
- 是当前节点表示不了的、是当前节点能表示的子串集合的后缀。
- 若当前节点能表示的最短子串为 ,那么指向的节点能表示的最长子串一定为 。
- 以下是上图中的 树,括号代表 当前节点表示的子串集合的长度范围
特征
- 父节点能表示的最短子串的长度 儿子节点能表示的最长子串的长度
- 每个父节点只连出一条 fail 边。
- 对于一个节点,如果该节点能表示的子串长度范围为 ,那么每一个长度短的一定是长度长的后缀
举例
- 注意看 6 号节点。
- 它能表示的子串集合为:、、 (能发现,每一个长度短的一定是长度长的后缀)。
- 它的 fail 边连向 7
- 7 能表示的最长子串为
- 该节点能表示的最长子串的长度
- 该节点能表示的子串集合的长度种类数:
性质
- 所有节点 从第一名到最后一名,或者从最后一名到第一名,既是 树 的拓扑序,又是 字典树 的拓扑序。
- 可以根据每个节点 的排名,直接拓扑排序进行DP转移,无需使用队列。
- 当前节点所能表示的子串集合 在 整个字符串 中出现的次数。
- 注意:对于任意一个节点,都不可能有:某节点既能表示出 ,又能表示出 的情况。
- 性质1:从某一点出发,沿着字典树往下走, 只可能不变或递减。
- 性质2:从某一点出发,逆着fail边往下走, 只可能不变或递减。
它是怎么求出来的?
- 拓扑排序。按照 从 最大到最小,进行拓扑排序。
- 初始:各节点 。
- 转移:,其中 ,并且 是 通过 fail 边连向的节点。
- 代码:
for(int i=tot;i>=1;i--)
{
int cur = rnk_len[i];
sz[ fail[cur] ] += sz[cur];
}
例题
洛谷3975 - 弦论
题目描述
- 对于一个给定的长度为 的字符串,求出它的第 小子串。
- 第一行是一个仅由小写英文字母构成的字符串 。
- 第二行为两个整数 和 , 为 则表示不同位置的相同子串算作一个, 为 则表示不同位置的相同子串算作多个。
解析
- 每个节点都代表一个子串的集合,这是从 DAG(字典树) 根节点到这个点所有可能走过的路径构成的字符串的集合。
- 并且在 DAG 的某个节点上,继续往下走还会走到后续的节点。
- 因此,对于 DAG 的每个节点,我们都需要统计出在 当前点和当前点的后续节点,总共有多少子串。
- 自然联想到拓扑排序。我们知道,
所有节点 rank_max_len 从第一名到最后一名,或者从最后一名到第一名,既是 fail 树 的拓扑序,又是 字典树 的拓扑序。
所以,按照拓扑序进行转移即可。 - 转移:
- 当 时,初始化 ,转移 , 是字典树拓扑序靠后的。
- 当 时,初始化 ,转移 , 是字典树拓扑序靠后的。
- 最后 DFS 遍历输出即可。
牛客33188H多校 - Hacker
https://ac.nowcoder.com/acm/contest/33188/H
简化版描述
- 给出一个字符串 ,
- 有 组询问,每次给出一个字符串 ,对于每个 ,求出 的 最长的、和 匹配的最长子串的长度。
解析
- 建立后缀自动机。
- 刚开始,直接字典树匹配。
- 如果失配,则沿 边跳至其它节点,直到能继续匹配了为止。
牛客37092C - 葫芦的考验之定位子串
描述
- 给出一个只包含小写字母的字符串 。有 次询问,每次询问 的 在 中的出现次数。
解析
- 后缀自动机不能直接处理指定区间子串的问题,
- 但是我们可以利用 边的性质。
- 我们知道, 边指向的是
当前节点表示不了的、是当前节点能表示的子串集合的后缀。
并且,若当前节点能表示的最短子串为 c+S,那么指向的节点能表示的最长子串一定为 S。
- 并且,从某个节点出发,沿着 边走,途经的每个节点表示的都是上一个节点的后缀,并且互相没有交集,并且表示的子串集合的长度单调递减,没有交集。
- 所以,朴素的做法是:
- 对于区间 ,设长度 ,先定位到 对应的字典树节点,
- 然后,设 边连向的点为
- 如果 ,那么跳至 ,重复上述过程。
- 直到 ,此时当前点(非 )表示的子串集合就包含了区间 的字符串。
- 优化方法:倍增,使用 ST 表。
建表
for (int i=1; i<=tot; i++)
ST[i][0] = fail[i];
for (int i=1; i<=19; i++)
{
for (int j=1; j<=tot; j++)
{
ST[j][i] = ST[ ST[j][i-1] ][i-1];
}
}
查表
- 继续跳表的条件是 。
for (int i=19; i>=0; i--)
{
int nxt = ST[cur][i];
if(!nxt)continue;
if(max_len[nxt] >= len)
{
cur = nxt;
}
}
printf("%d\n",sz[cur]);
CF149E - Martian Strings
题意
- 给定一个长度为 的字符串 ,该串仅由大写字母组成
- 给定 个询问,每行一个字符串 ,满足 互不相同
- 对于每个询问,问是否存在 满足 且 ,即可以找到两个不相交的区间,满足这两个区间对应的子串拼起来和 相同。
解析
- 建立两个后缀自动机, 是 ,是 的反串。
错误做法
- 建立后缀自动机后,对于两个自动机,查模版中的 vector 数组 。
- 错误原因:内存超限。
正确做法
- 首先来看一下模版中的 vector 数组 的求法。
end_pos[cur].push_back(pos);
- 在插入字符时,令 , 是当前插入字符的位置。
- 全部字符插入完成后,根据拓扑序,对于每个点,向 边连向的、拓扑序靠后的节点的 插入该点。
for(int i=tot;i>=1;i--)
{
int cur = rnk_len[i];
for(auto num : end_pos[cur])
end_pos[ fail[cur] ].push_back(num);
}
- 改造思路:去掉这个 vector 数组,我们只关心每个节点 的最大值和最小值。
- 我们发现,如果对于每个节点的 ,只存储它 的最大值和最小值,上述 【向拓扑序靠后的节点的 插入该点】的代码只需改成 【向拓扑序靠后的节点的 更新该点 的最大值和最小值】 也可以。
for(int i=tot;i>=1;i--)
{
int cur = rnk_len[i];
maxi[fail[cur]] = max(maxi[fail[cur]], maxi[cur]);
mini[fail[cur]] = min(mini[fail[cur]], mini[cur]);
// for(auto num : end_pos[cur])
// end_pos[ fail[cur] ].push_back(num);
}
牛客33186B多校 - Spirit Circle Observation
https://ac.nowcoder.com/acm/contest/33186/B
题意
- 给定一个长度为 的数字串 ,从中选出两个它的子串 ,满足:
- 长度相同;
- 。
- 若有两个子串长得相同,只要位置不同,则认为是不同的子串。
解析
初步思路
- 满足条件的 和 一定符合如下格式:
- 其中,第一部分 和 第三部分后缀
9
和0
可以为空。
进一步
- 枚举第一部分
void Sol()
{
Build();
ll ans = 0;
for (int i=1; i<=tot; i++)//枚举X
{
ans += Cal(i);
}
printf("%lld\n",ans);
}
- 对于每个 ,通过字典树查询紧接着 的 和 。
- 设紧接着 的 对应的节点为, 对应的节点为 。
- 如果成功同时找到了 和 ,那么对答案的贡献为 ,其中
- 沿着字典树接着往下走, 继续搜寻
9
, 继续搜寻0
。 - 如果成功同时找到,那么对答案的贡献为 (同上)。
- 直到某一个找不到接下来的
9
或0
,宣告停止。
ll Cal(int cur)
{
ll cnt = max_len[cur] - max_len[ fail[cur] ];
if(cur==1) cnt = 1;
ll sum = 0;
for (int i=0; i<=8; i++)
{
//对于每个 X,通过字典树查询紧接着 X 的 digit 和 digit+1。
int nxt1 = trie[cur][i];
int nxt2 = trie[cur][i+1];
if(nxt1==0 || nxt2==0)continue;
//如果成功同时找到了 nxt1 和 nxt2,那么对答案产生贡献
sum += cnt * sz[nxt1] * sz[nxt2];
while (1)
{
nxt1 = trie[nxt1][9];
nxt2 = trie[nxt2][0];
if(nxt1==0 || nxt2==0)break;
//如果成功同时找到了 nxt1 和 nxt2,那么对答案产生贡献
sum += 1ll * cnt * sz[nxt1] * sz[nxt2];
}
}
return sum;
}
答疑
- 这样会不会造成计算重复?
- 不会。因为我保证在第一部分和第三部分之间有 。
- 为什么贡献是 ?
- 因为我枚举的是后缀自动机的每个节点,枚举每个节点能保证,所有子串必然枚举恰好一次。
- 设这个原串总共有 个子串(本题规定相同但位置不同的子串不算同一个,那么 )对于后缀自动机的某个节点 ,它能表示的子串集合数量为 ,那么 。