SAM 简记
SAM 可以表示一个字符串的所有子串。表示的意思是可以用从起点到某一点的路径表达。
SAM 是自动机,若它能表示一个字符串,则显然可以表示这个字符串的前缀。
所以,只要表示出所有的后缀,就能表示所有子串。
对于 \(s\) 的一个子串 \(t\),\(\text{endpos}(t)\) 表示其在 \(s\) 出现的结束位置集合。例如 \(s=\texttt{abcbc}\) 时,\(\text{endpos}(\texttt{bc})=\{3,5\}\)。
显然,若两个子串的 \(\text{endpos}\) 有交,说明一个字符串出现的地方就有另一个字符串,所以其中一个是另一个的后缀,故两个 \(\text{endpos}\) 有包含关系。
所以,两个子串的 \(\text{endpos}\) 要么没有交,要么有包含关系。
\(\text{endpos}\) 相同的子串中,长度一定是一段连续的数字。因为其中最长的子串的 \(\text{endpos}\) 被比它短的包含,最短的子串的又包含所有比它长的子串,所以对于 \(\text{endpos}\) 相等的长度为 \(L_1<L_2\) 的两个子串,一定有长度为 \(L_1+1,\cdots,L_2-1\) 的子串的 \(\text{endpos}\) 与之相等。
我们可以把 \(\text{endpos}\) 相同的子串归为一类。在这一类中最长的字符串前面加一个字符,其 \(\text{endpos}\) 不同,但是被包含。加不同的字符可能会导出不同的 \(\text{endpos}\),根据上面所述,它们没有交。所以可以通过在最长的字符前面加字符的方式拆分 \(\text{endpos}\) 集合。当然,不是所有集合中的数字都一定会保留下来。
所以我们可以看出一个树结构:根节点代表 \(\text{endpos}=\{1,2,\cdots,|s|\}\) 的一类,这一类长度最小的是空串。然后可以通过向最长字符串前加字符的方式拆分成几个集合。我们叫这棵树 parent tree。
parent tree 的节点和 SAM 的节点意义一致(但是边不一致),所以我们可以把一个节点当做一个状态。其中 \(\text{endpos}\) 包含 \(|s|\) 的链上的点被称为终止状态,包含了 \(s\) 的后缀。在这棵树上从儿子到父亲的边被称为后缀链接,用 \(\text{link}(u)\) 表示,可以跳到自己的后缀。用 \(\text{str}(u)\) 表示状态 \(u\) 中的最长字符串,\(\text{len}(u)=|\text{str}(u)|\)。
显然,状态 \(u\) 中最短字符串的长度为 \(\text{len}(\text{link}(u))+1\)。
而 SAM 则用转移将点连接起来。转移是在 \(\text{str}(u)\) 的 后面 添加字符得到的字符串对应的另一状态。
构造 SAM 的方式也是将字符一个个加入。对于每个状态,我们保存转移、\(\text{link}\)、\(\text{len}\)。初始时只有一个状态,编号为 \(0\)。
int cur=++tot,u=lst;
len[cur]=len[lst]+1;lst=cur;
for(;~u&&!t[u][c];u=f[u])t[u][c]=cur;
if(u==-1)return f[cur]=0,void();
从原来表示整个字符串的状态开始,不断跳后缀链接,直到有 \(c\) 的转移。没有 \(c\) 的转移时,将其连接到 \(\text{cur}\)。\(\text{cur}\) 的意义是加入这个字符后表示整个字符串的状态。一旦有 \(c\) 转移,显然其后缀也必然有。f 表示后缀链接。若跳出去了,表明新字符串在老字符串中没有后缀,直接赋值为 0。否则,\(\text{str}(\text{link}(\text{cur}))=\text{str}(u)+c\)。
int v=t[u][c];
if(len[u]+1==len[v])return f[cur]=v,void();
如果状态 \(v\) 恰好满足这个条件,说明 \(\text{link}(\text{cur})=v\)。
int p=++tot;len[p]=len[u]+1;f[p]=f[v];
memcpy(t[p],t[v],sizeof(t[p]));
for(;~u&&t[u][c]==v;u=f[u])t[u][c]=p;
f[v]=f[cur]=p;
否则必须将 \(v\) 拆成两个点,即将 \(\text{str}(u)+c\) 及其后缀分入 \(p\),其他的留在 \(v\),另一个点 \(p\) 复制 \(v\) 除了 \(\text{len}\) 的信息。造出了我们需要的 \(p\) 后,需要把 \(v\) 后面的所以最后将 \(u\) 及其后缀到 \(v\) 上的转移调整到 \(p\),\(\text{link}(v)=p\)。
对于时间复杂度,我不懂。也许以后会补上。
做 P3804 【模板】后缀自动机 (SAM) 时还有个问题就是计算每个点的 \(\text{endpos}\) 的大小。可以把每个可能的元素即 \(1,2,\cdots,|s|\) 预先放入节点中。具体地,把每个前缀对应的状态的 \(\text{size}\) 加一,然后遍历 parent tree 得出大小。
#include<bits/stdc++.h>
using namespace std;
constexpr int N=2e6+5;
int tot,lst,f[N],t[N][26],len[N],siz[N];
vector<int>e[N];long long ans;
void dfs(int u){
for(int v:e[u])dfs(v),siz[u]+=siz[v];
if(u&&siz[u]>1)ans=max(ans,1ll*siz[u]*len[u]);
}
int main(){
f[0]=-1;string s;cin>>s;
for(int c:s){
int cur=++tot,u=lst;c-='a';siz[cur]++;
len[cur]=len[lst]+1;lst=cur;
for(;~u&&!t[u][c];u=f[u])t[u][c]=cur;
if(u==-1){f[cur]=0;continue;}
int v=t[u][c];
if(len[v]==len[u]+1)f[cur]=v;else{
int p=++tot;len[p]=len[u]+1;f[p]=f[v];
memcpy(t[p],t[v],sizeof(t[p]));
for(;~u&&t[u][c]==v;u=f[u])t[u][c]=p;
f[v]=f[cur]=p;
}
}
for(int i=1;i<=tot;i++)e[f[i]].push_back(i);
dfs(0);cout<<ans<<'\n';
return 0;
}
SAM 可以理解为对一个字符串的所有子串建立的 ACAM。

浙公网安备 33010602011771号