广义后缀自动机
广义后缀自动机
感觉挺水的,大致上和后缀自动机没有什么区别,只是加了几句特判。
1 离线做法
我们先对所有的字符串建一棵 Trie
树,然后对这颗 Trie
树建立后缀自动机。
如何建立呢?我们只需要对这颗 Trie
树进行 bfs 或 dfs ,不断往自动机里面加点就可以了。
唯一不同的是,节点的 \(last\) 要为其父节点,所以要储存 Trie
树上每一个节点在后缀自动机上的编号。
注意,如果是给了你一棵 Trie
树,那么 dfs
做法可能会被卡成 \(O(n^2)\) ,其中 \(n\) 是所有字符串的长度总和。
至于为什么会被卡,感兴趣的读者可以看我在引用部分挂的链接,这里不再赘述。
插入部分和普通 SAM 的插入相同。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 5000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
struct SAM{
struct node{
int link,ch[26],len;
};
node p[N];
int size,tr[N][26],pos[N],c[N],fa[N];
queue<int> q;
inline SAM(){p[0].len=0;p[0].link=-1;}
inline void trie_insert(char *s){
int now=0,len=strlen(s);
for(int i=0;i<len;i++){
if(!tr[now][s[i]-'a']) {tr[now][s[i]-'a']=++size;c[size]=s[i]-'a';fa[size]=now;}
now=tr[now][s[i]-'a'];
}
}
inline int insert(int c,int last){
int cur=++size,now=last;
p[cur].len=p[now].len+1;
while(now!=-1&&!p[now].ch[c]) p[now].ch[c]=cur,now=p[now].link;
if(now==-1) p[cur].link=0;
else{
int q=p[now].ch[c];
if(p[q].len==p[now].len+1) p[cur].link=q;
else{
int clone=++size;
p[clone]=p[q];p[clone].len=p[now].len+1;
while(now!=-1&&p[now].ch[c]==q) p[now].ch[c]=clone,now=p[now].link;
p[q].link=p[cur].link=clone;
}
}
return cur;
}
inline void build(){
for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
pos[1]=0;
while(q.size()){
int top=q.front();q.pop();
pos[top]=insert(c[top],pos[fa[top]]);
for(int i=0;i<26;i++) if(tr[top][i]) q.push(tr[top][i]);
}
}
inline ll ask_sum(){
ll res=0;
for(int i=1;i<=size;i++) res+=p[i].len-p[p[i].link].len;
return res;
}
};
SAM sam;
int n;
char s[N];
int main(){
read(n);
for(int i=1;i<=n;i++){
scanf("%s",s);sam.trie_insert(s);
}
sam.build();
ll ans=sam.ask_sum();
printf("%lld\n",ans);
return 0;
}
这个做法的正确性来自于 Trie
树本身就储存了大量的 lcp 信息,每一个放进去的节点都可以看做是一个被压缩的子串,所以正确性感性理解一下应该是比较显然的。
2 在线做法
给定 \(n\) 个字符串 \(s_1,s_2,...s_n\) ,要求我们在线构造广义后缀自动机。
首先我们每加入一个串,\(last\) 都要重置到自动机源点,这个比较好理解,毕竟这些串不是连起来的。
那么我们这样做会带来一个问题,就是 ch[last][c]
可能已经有数了,这个怎么办?
我们先放代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 3000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
struct node{
int link,ch[26],len;
};
node p[N];
struct SAM{
int size,last;
inline SAM(){p[0].link=-1;p[0].len=0;last=0;}
inline void add(char *s){
last=0;int len=strlen(s);
for(int i=0;i<len;i++) last=insert(s[i]-'a');
}
inline int insert(int c){
if(p[last].ch[c]){
int q=p[last].ch[c],now=last;
if(p[now].len+1==p[q].len) return q;
else{
int clone=++size;
p[clone]=p[q];p[clone].len=p[now].len+1;
while(now!=-1&&p[now].ch[c]==q) p[now].ch[c]=clone,now=p[now].link;
p[q].link=clone;
return clone;
}
}
int cur=++size;p[size].len=p[last].len+1;
int now=last;
while(now!=-1&&!p[now].ch[c]) p[now].ch[c]=cur,now=p[now].link;
if(now==-1) p[cur].link=0;
else{
int q=p[now].ch[c];
if(p[now].len+1==p[q].len) p[cur].link=q;
else{
int clone=++size;
p[clone]=p[q];p[clone].len=p[now].len+1;
while(now!=-1&&p[now].ch[c]==q) p[now].ch[c]=clone,now=p[now].link;
p[q].link=p[cur].link=clone;
}
}
return cur;
}
inline ll ask_sum(){
ll res=0;
for(int i=1;i<=size;i++){
res+=p[i].len-p[p[i].link].len;
}
return res;
}
};
SAM sam;
ll n,ans;
char s[N];
int main(){
read(n);
for(int i=1;i<=n;i++){
scanf("%s",s);sam.add(s);
}
ans=sam.ask_sum();
printf("%lld\n",ans);
return 0;
}
相信读者已经发现,我们的特判与我们后缀自动机拆点的步骤非常相像。而特判的本质是看一看是否有必要加入这个节点。如果你已经十分了解后缀自动机了,那么其实如果有 p[now].len+1==p[q].len
,那么 \(q\) 实际上已经把我们将要加入的这个节点完全替代了。
否则,那我们需要再建立一个 \(len\) 为 p[now].len+1
的节点,发现这个问题和我们拆点的问题非常像,所以我们可以直接参考我们在拆点那里的代码。
3 复杂度分析
根据后缀自动机的复杂度,这个复杂度应该是线性的,在实际实现中,因为离线算法要建 Trie
树,所以离线算法要比在线算法常数大。
如果广义后缀自动机构建正确,离线算法和在线算法构造的时间复杂度应该是相同的。