SAM初学笔记
我第一次学习SAM,可能有很多地方理解有偏差或者错误。还请各位大佬指正。这篇文章中没有严谨的证明,只有感性的理解。追求严谨的请转他处。
首先我们定义Parent树:一个节点与其所代表的字符串的最长的,且出现次数与其不一样的后缀连边,所形成的树是Parent树。
可以发现Parent树有很多优秀的性质,比如子树内的节点数即为这个串出现次数等。
我们基本的SAM要维护三个值:父亲,当前点长度,儿子。注意这里的儿子是广义上的儿子,即这个儿子所代表的不仅是父亲为它的节点,而是所有他有出边的节点。因为SAM是一张图。
SAM有一个性质,就是从源点到所有点的所有路径,就是所有的子串。
如何构建SAM呢?
如果我们要往SAM中加入一个字符\(c\),先处理好\(len\),我们从上一次插入的地方开始将它沿Parent树往上跳。直到跳到有一个节点有一个儿子也是\(c\)
在这个跳的过程中,我们将一路上的节点都添加一个儿子\(c\),这个怎么理解呢?就是将这些后缀末尾都增加一个\(c\),而为什么只有这一路要加呢?因为剩下的均不是出现次数与其不一样的后缀。
然后我们找到这个节点\(p\)与其儿子\(q\),这时分类讨论:
如果\(len_q=len_p+1\),那么\(p\)也为当前的总串\(s\)的后缀,所以将\(c\)指向\(p\)
否则我们发现字符串集\(p\)中有一个是总串\(s\)的后缀,而其他不是。为了避免混淆而引起的错误,我们可以把\(p\)拆开,分出一个\(g\),同时\(g\)的儿子信息与\(q\)的一样。因为根据定义,\(g\)也是\(q\)的后缀,所以后面加上一个是不会改变的。
但是这时我们把它拆开了不就会少掉吗?所以我们把\(q\)指向\(g\),\(g\)指向\(p\),同时把上面所有的儿子为\(p\)的指向\(g\),如果不这样同样会少掉答案。而关于所有儿子为\(p\)的节点肯定是连续的,因为肯定是一个字符加入是不断往上跳改变出来的。
而此时\(g\)就满足了\(len_g=len_p+1\),那么就可以把\(c\)接在\(g\)后面了。
不得不说整个SAM还是很巧妙的。
放一下模板题的code:
#include<cstdio>
#include<cstring>
#define ll long long
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
int n,m,k,x,y,z,cnt=1,last=1,now,p,cur,pus,g[2000039],w[2000039],dp[2000039],ans;
char s[1000039];
struct SAM{int len,link,son[26];}f[2000039];
inline void insert(int x){
p=last;now=last=++cnt;f[now].len=f[p].len+1;dp[cnt]=1;
while(p&&!f[p].son[x]) f[p].son[x]=now,p=f[p].link;
if(p){
cur=f[p].son[x];
if(f[cur].len==f[p].len+1)f[now].link=cur;
else {
f[pus=++cnt]=f[cur];f[pus].len=f[p].len+1;f[cur].link=f[now].link=pus;
while(p&&f[p].son[x]==cur)f[p].son[x]=pus,p=f[p].link;
}
}
else f[now].link=1;
}
int main(){
freopen("1.in","r",stdin);
register int i;
scanf("%s",s+1);n=strlen(s+1);
for(i=1;i<=n;i++) insert(s[i]-'a');
for(i=1;i<=cnt;i++) g[f[i].len]++;
for(i=1;i<=cnt;i++) g[i]+=g[i-1];
for(i=1;i<=cnt;i++) w[g[f[i].len]--]=i;
for(i=cnt;i;i--)dp[f[w[i]].link]+=dp[w[i]],(dp[w[i]]^1)&&(ans=max(ans,dp[w[i]]*f[w[i]].len));
printf("%d\n",ans);
}