后缀自动机
作者:互联网
(本文不适合初学者)
SAM
-
个人认为 SAM yyds
-
希望有一天 SAM 能统治字符串界
前置概念
-
\(\operatorname{endpos}\) 集合表示一个子串在原串中出现的位置集合
-
所有的子串通过 \(\operatorname{endpos}\) 分成一个个等价类
构造
-
每个节点代表一个子串集合(或者看成是一种状态),状态之间有一些字符转移边
-
每个节点维护集合最长的字符串的长度
-
每个节点维护的子串集合实际上就是集合中的最长子串的一些前缀
-
每个节点维护每个字符转移边的节点
-
每个节点维护后缀链接指向集合中的最长子串的最长的和这个子串的 \(\operatorname{endpos}\) 不一样的后缀所在的节点,并且满足 \(\operatorname{minlen(x)}=\operatorname{maxlen(link(x))}+1\)
namespace SAM{
struct node{
int len,link,nxt[26];
}a[N<<1];
int las=0,tot=0;
inline void init() {a[0].link=-1;}
inline void insert(int x) {
int cur=++tot;cnt[cur]=1;
a[tot].len=a[las].len+1;
int p=las;
while(p!=-1 && !a[p].nxt[x]) {a[p].nxt[x]=cur;p=a[p].link;}
if(p==-1) a[cur].link=0;
else {
int q=a[p].nxt[x];
if(a[p].len+1==a[q].len) a[cur].link=q;
else {
int clone=++tot;
a[clone]=a[q];
a[clone].len=a[p].len+1;
a[q].link=a[cur].link=clone;
while(p!=-1 && a[p].nxt[x]==q) {a[p].nxt[x]=clone;p=a[p].link;}
}
}
las=cur;
}
}
(注意空间要开两倍)
性质
-
每个前缀的终止节点不同
-
每个节点所代表的子串集合一定是一段 “连续” 的字符串,形式化的说集合中的每个子串都是最长子串的一个后缀,并且长度连续
应用
广义 SAM
-
大概是用来解决多个模式串
-
我们可以认为每个字符串的每个位置都是独一无二的,也就是在算等价类的时候是不同的
-
但是为了尽可能的压缩我们的信息,每新插入一个新的字符串的时候,就从头插入
-
其余的构造和普通的 SAM 是一样的
namespace SAM{
struct node{
int link,len,nxt[26];
}a[N<<1];
int tot,las=0;
inline void init() {a[0].link=-1;}
inline void insert(int x) {
if(a[las].nxt[x]) {
int cur=a[las].nxt[x];
if(a[las].len+1==a[cur].len) return las=cur,void();
int clone=++tot;
a[clone]=a[cur];a[clone].len=a[las].len+1;
a[cur].link=clone;
int p=las;
while(p!=-1 && a[p].nxt[x]==cur) a[p].nxt[x]=clone,p=a[p].link;
return las=clone,void();
}
int cur=++tot;
a[cur].len=a[las].len+1;
int p=las;
while(p!=-1 && !a[p].nxt[x]) a[p].nxt[x]=cur,p=a[p].link;
if(p==-1) a[cur].link=0;
else {
int q=a[p].nxt[x];
if(a[p].len+1==a[q].len) a[cur].link=q;
else {
int clone=++tot;
a[clone]=a[q];
a[clone].len=a[p].len+1;
a[q].link=a[cur].link=clone;
while(p!=-1 && a[p].nxt[x]==q) a[p].nxt[x]=clone,p=a[p].link;
}
}
las=cur;
}
}using namespace SAM;
性质
-
如果将所有模式串去重后,每个串的终止节点一定不一样,并且一定代表着所在节点集合的最长子串
-
一个比较感性的说法(尽量理解因为很重要):对于一个节点所代表的集合的 “范围” 是任意一个它能转移到的状态所代表的集合的 “范围” 的子集,这里的 “范围” 抽象理解,主要是为了证明下面所说的
-
目前我的观点是 AC 自动机可以做的,广义 SAM /SAM 也可以做
-
观察 AC 自动机每个节点维护的是 \(fail\) 指针,也就是失配指针,而 SAM 的每个节点的 \(link\) 其实是和失配指针有着差不多的一个意思
-
但是不能直接按照这么做,这是因为 SAM 的结构还是和 AC 自动机有所不同的
-
我们转移的时候,对于一个节点是否有贡献一定要注意判断,因为假设当前 \(p\) 转移到 \(a[p].nxt[x]\) ,但是 \(a[p].len!=a[a[p].nxt[x]].len\) ,这说明还有其他的子串可以转移到这个点,但是这个贡献显然是不能算上去的,所以这里要根据每个题目去判断一下
-
还是需要根据代码理解
- 重点在于理解
int p=0,fl=1;
FOR(i,1,len) {
int x=S[i]-'a';
while(p!=-1 && !a[p].nxt[x]) p=a[p].link,fl=1;
if(p==-1) p=0;
else {
if(a[p].len+1!=a[a[p].nxt[x]].len) fl=0;
p=a[p].nxt[x];
if(!fl) ++num[a[p].link];
else ++num[p];
}
}
应用
标签:子串,每个,SAM,后缀,len,集合,自动机,节点 来源: https://www.cnblogs.com/kzos/p/16343090.html