三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

SAM 学习笔记

SAM 学习笔记

推荐资料

以入门为主
OI-wiki
这篇以应用为主
【学习笔记】字符串—广义后缀自动机

cplusoj的题单
自动机相关

简介

后缀自动机(SAM)我更愿称之为子串自动机,一个节点\(u\) 代表多个互为后缀关系的串,其长度在 \(len_{fail_u}\)\(len_u\),且出现的 endpos 集合相同。当前的新建的节点 \(u\) 对应其实的是加入位置 \(i\) 的一个后缀。\(lst\) 维护的是以上一位结尾的最长后缀,其实就是 \(s[1,n]\),这里感性理解就行,没必要揪着不放。

与 ACAM 一样,它的核心也是 fail 数组,\(fail_u\) 表示的是当前 \(u\) 代表的子串的一个最长真后缀,同时,SAM 维护了类似 ACAM 的 \(tr\) 数组,其实现的功能是,\(u\) 的所有串后加字符 \(c\) 得到的串都在\(tr_{u,c}\) 内。

引入

请你求出小写字符串 \(S\) 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。

解法

构建出 SAM 之后,在 \(fail\) 树上计算子树大小。

直接看代码吧!背背板子。

构建 SAM 的复杂度是 \(O(n|\sum|)\),使用 \(map\) 后是 \(O(n\log|\sum|)\)

code
const int N=2e6+5;int n,tr[N][26],cnt[N],fail[N],len[N],tot=1,lst=1;
string s;
int nxt[N],head[N];
ll ans;void ins(int x){int u=++tot,p=lst;len[u]=len[p]+1,cnt[u]=1;while(p && !tr[p][x]) tr[p][x]=u, p=fail[p];if(!p) fail[u]=1;else{int q=tr[p][x];if(len[q]==len[p]+1) fail[u]=q;else{int cq=++tot;len[cq]=len[p]+1;fail[cq]=fail[q];fail[u]=fail[q]=cq;memcpy(tr[cq],tr[q],sizeof(tr[q]));while(p && tr[p][x]==q) tr[p][x]=cq, p=fail[p];}}lst=u;
}void dfs(int u){for(int v=head[u];v;v=nxt[v]) dfs(v),cnt[u]+=cnt[v];if(cnt[u]>1) ans=max(ans,1ll*cnt[u]*len[u]);
}signed main(){                          IOS             cin>>s;n=s.length();s=" "+s;for(int i=1;i<=n;i++) ins(s[i]-'a');for(int i=1;i<=tot;i++) nxt[i]=head[fail[i]],head[fail[i]]=i;   dfs(1);cout<<ans<<"\n";return 0;
}

注意事项

  1. SAM 的点数不超过 \(2n-1\),边数(\(tr\) 有效位置)据称不超过 \(3n-4\),所以有关 SAM 的所有数组都要开 2 倍空间

  2. 在写代码尤其是新建节点的时候,一定要看清每个相关数组是否赋值了。

应用

两个后缀的 LCP

对反串建 SAM,那么两个后缀的 lcp 就相当于 SAM 树上的 lca。

endpos 集合

对于一个前缀 $ s[1, i] $ 所对应的节点 $ u_i $,其会使得 $ u_i $ 的所有 fail 树祖先都在 $ i $ 处出现过。因此可以使用线段树合并维护每个节点对应的 endpos 集合,线段树维护的是该区间有多少个 endpos。

不同子串个数

\(\sum_{u=1}^{tot} len_u-len_{fail_u}\)

走路操作

维护了长为 \(l\) 的字符串 \(t\),在 \(s\) 正串 SAM 的 \(u\) 节点,满足

\[len_{fail_u} < l \leq len_u: \]

后加字符 \(c\):跳 \(tr_{u,c}\)
前删字符 \(c\):如果 \(l - 1 = len_{fail_u}\),则跳 \(fail_u\),反之不动。
前加字符 \(c\):如果 \(l = len_u\),则跳至 \(son_{u,c}\),反之不动,\(son\) 其实是反向 \(fail\)
\(son_{u,c}\) 处理方法:枚举 \(v\)\(son_{fail_v,s[R(v)-len(u)]} = v\)

题目

CF235C Cyclical Quest

题目大意

给定一个文本串 \(s\) 和若干个模式串 \(t_i\),对于每个 \(t_i\),求出 \(s\) 中有多少个子串与它循环同构。

解题思路

\(t_i\) 复制一份拼在一起,双指针在 SAM 上走路即可,只需要前删和后加。

CF700E Cool Slogans

题目大意

给定一个字符串 \(s\),你需要求出一个最长的字符串序列,使得每一个串都在后一个串至少出现两次。

解题思路

首先总是可以让前一个是后一个的后缀,这样肯定不劣。建出 SAM,线段树合并维护 endpos。对 fail 树进行从上往下的贪心(其实也可以说是 dp,毕竟这是一个内向树)。

\(u\) 后面能接 \(v\)(这里 \(u\)\(v\) fail树上的父亲,即它的后缀)当且仅当在对于 \(v\) 任意一个 endpos,因为 \(u\) 对应的子串,肯定在 endpos 这个位置出现过,所以我们只需要判断在 \([endpos-len_v+1+len_u-1,endpos-1]\) 这个位置是否出现过即可。我们只需要找 \(v\) 的任意一个 endpos,因为他们彼此之间是等价的。

← 返回列表