推荐学习:%%DZYO%%%
我就只贴一个模板了……【例题在后面】
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
struct node{
int link,len;
//link后缀链接所指向的自己的后缀
//len当前中点等价类中长度最长的哪一个的长度
int nxt[28];
//后缀自动机(DAG)所指向的
}st[N<<1];//后缀自动机兼后缀链接
int siz,last;
//siz一共的节点数
//last上一次加入的状态
void sa_init(){//初始化
siz=1;last=1;
st[1].link=0;
st[1].len=0;
/*若关于不同的字符串多次建立后缀自动机,就需要执行这些代码:
for (int i=0; i<MAXLEN*2; ++i)
st[i].next.clear();*/
}
void sa_extend(int c){
//令last为对应整个字符串的状态(最初last=0,在每次字符添加操作后我们都会改变last的值)
int cur=++siz;int p;
st[cur].len=st[last].len+1;
//建立一个新的状态cur,令len(cur)=len(last)+1,而link(cur)的值并不确定
for(p=last;p&&!st[p].nxt[c];p=st[p].link)
st[p].nxt[c]=cur;//将之前子串的后缀都加上当前字符( 添加字符c的转移)
//我们最初在last,如果它没有字符c的转移,那就添加字符c的转移,指向cur,
//然后走向其后缀链接,再次检查——如果没有字符c的转移,就添加上去
//如果在某个节点已有字符c的转移,就停止,并且令p为这个状态的编号
//如果“某节点已有字符c的转移”这一事件从未发生,而我们来到了空状态-1(经由t_0的后缀指针前来)
//我们简单地令link(cur)=0,跳出
if(p==0)st[cur].link=1;
else{
//假设我们停在了某一状态q,是从某一个状态p经字符c的转移而来
//现在有两种情况:len(p)+1=len(q)或不然
int q=st[p].nxt[c];
//如果len(p)+1=len(q),那么我们简单地令link(cur)=q,跳出
if(st[q].len==st[p].len+1)st[cur].link=q;
//p是last的后缀 如果q只比p多了一个字符(当前字符c)
//说明q是cur的后缀,于是就连一条边
else{
//否则,情况就变得更加复杂。必须新建一个q的“拷贝”状态:
//建立一个新的状态clone,将q的数据拷贝给它(后缀链接,以及转移)
//除了len的值:需要令len(clone)=len(p)+1
int clone=++siz;
st[clone].link=st[q].link;
for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];//memcpy要挂
st[clone].len=st[p].len+1;
for(;p&&st[p].nxt[c]==q;p=st[p].link)
st[p].nxt[c]=clone;
//最终,我们需要做的最后一件事情就是——从p开始沿着后缀链接走,
//对每个状态我们都检查是否有指向q的,字符c的转移,
//如果有就将其重定向至clone(如果没有,就终止循环)
st[q].link=st[cur].link=clone;
//在拷贝之后,我们将cur的后缀链接指向clone,并将q的后缀链接重定向到clone
}
}
last=cur;
//在任何情况下,无论在何处终止了这次添加操作
//我们最后都将更新last的值,将其赋值为cur
}
//如果我们还需要知道哪些节点是终止节点而哪些不是,我们可以在构建整个字符串的后缀自动机之后找出所有终止节点。
//对此我们考虑对应整个字符串的节点(显然,就是我们储存在变量last中的节点),
//我们沿着它的后缀链接走,直到到达初始状态,并且将途径的每个节点标记为终止节点。
//很好理解,如此我们标记了字符串s所有后缀的对应状态,也就是我们想要找出的终止状态。
int s[N],lens;
int main(){
scanf("%s",s+1);
lens=strlen(s+1);
sa_init();
for(int i=1;i<=lens;i++)
sa_extend(s[i]-'a');
retrun 0;
}
- 这个模板是从 1 1 1开始用点的,因为从 0 0 0开始的话, 0 0 0的 l i n k link link会指向 − 1 -1 −1,用数组时会越界。
- 下面刚开始是从 0 0 0开始的,就不要介意板子不一样了,改过来就好。
下面讲讲例题。
后缀自动机模板
先建立后缀自动机,将所有不是克隆的点都染色,然后通过 l i n k link link树,求一下子树大小就是出现了多少次。然后再用当前节点的 l e n len len(最大长度)*出现次数,取个 m a x max max就好了。
我是用 建 边 + D F S 建边+DFS 建边+DFS来统计的。
// luogu-judger-enable-o2
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
struct node{
int link,len;
bool cl;
//link后缀链接所指向的自己的后缀
//len当前中点等价类中长度最长的哪一个的长度
int nxt[30];
//后缀自动机(DAG)所指向的
}st[N<<1];//后缀自动机兼后缀链接
int siz,last;
//siz一共的节点数
//last上一次加入的状态
void sa_init(){//初始化
siz=1;last=0;
st[0].len=0;
st[0].link=-1;
/*若关于不同的字符串多次建立后缀自动机,就需要执行这些代码:
for (int i=0; i<MAXLEN*2; ++i)
st[i].next.clear();*/
}
void sa_extend(int c){
//令last为对应整个字符串的状态(最初last=0,在每次字符添加操作后我们都会改变last的值)
int cur=siz++;int p;
st[cur].len=st[last].len+1;
//建立一个新的状态cur,令len(cur)=len(last)+1,而link(cur)的值并不确定
for(p=last;p!=-1&&!st[p].nxt[c];p=st[p].link)
st[p].nxt[c]=cur;//将之前子串的后缀都加上当前字符( 添加字符c的转移)
//我们最初在last,如果它没有字符c的转移,那就添加字符c的转移,指向cur,
//然后走向其后缀链接,再次检查——如果没有字符c的转移,就添加上去
//如果在某个节点已有字符c的转移,就停止,并且令p为这个状态的编号
//如果“某节点已有字符c的转移”这一事件从未发生,而我们来到了空状态-1(经由t_0的后缀指针前来)
//我们简单地令link(cur)=0,跳出
if(p==-1)st[cur].link=0;
else{
//假设我们停在了某一状态q,是从某一个状态p经字符c的转移而来
//现在有两种情况:len(p)+1=len(q)或不然
int q=st[p].nxt[c];
//如果len(p)+1=len(q),那么我们简单地令link(cur)=q,跳出
if(st[q].len==st[p].len+1)st[cur].link=q;
//p是last的后缀 如果q只比p多了一个字符(当前字符c)
//说明q是cur的后缀,于是就连一条边
else{
//否则,情况就变得更加复杂。必须新建一个q的“拷贝”状态:
//建立一个新的状态clone,将q的数据拷贝给它(后缀链接,以及转移)
//除了len的值:需要令len(clone)=len(p)+1
int clone=siz++;
st[clone].cl=true;
st[clone].link=st[q].link;
for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];//memcpy要挂
st[clone].len=st[p].len+1;
for(;p!=-1&&st[p].nxt[c]==q;p=st[p].link)
st[p].nxt[c]=clone;
//最终,我们需要做的最后一件事情就是——从p开始沿着后缀链接走,
//对每个状态我们都检查是否有指向q的,字符c的转移,
//如果有就将其重定向至clone(如果没有,就终止循环)
st[q].link=st[cur].link=clone;
//在拷贝之后,我们将cur的后缀链接指向clone,并将q的后缀链接重定向到clone
}
}
last=cur;
//在任何情况下,无论在何处终止了这次添加操作
//我们最后都将更新last的值,将其赋值为cur
}
//如果我们还需要知道哪些节点是终止节点而哪些不是,我们可以在构建整个字符串的后缀自动机之后找出所有终止节点。
//对此我们考虑对应整个字符串的节点(显然,就是我们储存在变量last中的节点),
//我们沿着它的后缀链接走,直到到达初始状态,并且将途径的每个节点标记为终止节点。
//很好理解,如此我们标记了字符串s所有后缀的对应状态,也就是我们想要找出的终止状态。
char s[N];
long long ans;
struct edge{
int v,nxt;
}e[N<<1];
int first[N<<1],cnt=0;
void add(int u,int v){
e[++cnt].v=v;
e[cnt].nxt=first[u];first[u]=cnt;
}
int f[N<<1];
void dfs(int x){
for(int i=first[x];i;i=e[i].nxt){
dfs(e[i].v);
f[x]+=f[e[i].v];
}
if(!st[x].cl)f[x]++;
if(f[x]>1)ans=max(ans,(long long)f[x]*st[x].len);
}
int main(){
scanf("%s",s+1);
sa_init();
int le=strlen(s+1);//太慢,记录一下
for(int i=1;i<=le;i++)
sa_extend((int)(s[i]-'a'));
for(int i=1;i<siz;i++)
add(st[i].link,i);
dfs(0);
printf("%lld",ans);
return 0;
}
[TJOI2015]弦论
还是先建完后缀自动机,如果
t
=
=
0
t==0
t==0,就把每个点的权值赋值为
1
1
1。如果
t
=
=
1
t==1
t==1就向上一题那样记录出现了几次。不过,这次用的是桶排。
用后缀自动机的
D
A
G
DAG
DAG我们可以记录每一个节点有多少可以到达的子串,然后从后缀自动机的
D
A
G
DAG
DAG的初始节点向下跑,每次判断一下
k
k
k,就
O
K
OK
OK了。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
struct node{
int link,len,nxt[28];
}st[N<<1];
int siz,last;
void sa_init(){
siz=1;last=1;
st[1].link=0;
st[1].len=0;
}
int t,k,app[N<<1],sum[N<<1];//出现次数
void sa_extend(int c){
int cur=++siz;int p;
app[cur]=1;//
st[cur].len=st[last].len+1;
for(p=last;p&&!st[p].nxt[c];p=st[p].link)//
st[p].nxt[c]=cur;
if(p==0)st[cur].link=1;
else{
int q=st[p].nxt[c];
if(st[q].len==st[p].len+1)st[cur].link=q;
else{
int clone=++siz;
st[clone].link=st[q].link;
for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];
st[clone].len=st[p].len+1;
for(;p&&st[p].nxt[c]==q;p=st[p].link)//
st[p].nxt[c]=clone;
st[q].link=st[cur].link=clone;
}
}
last=cur;
}
char s[N];
int c[N<<1],rk[N<<1];
void work(){
for(int i=1;i<=siz;i++)c[st[i].len]++;
for(int i=1;i<=siz;i++)c[i]+=c[i-1];
for(int i=1;i<=siz;i++)rk[c[st[i].len]--]=i;//桶排
for(int i=siz;i>0;i--){
if(t)app[st[rk[i]].link]+=app[rk[i]];//st[0].link=-1越界
else app[rk[i]]=1;
}
app[1]=0;
for(int i=siz;i>0;i--){
sum[rk[i]]=app[rk[i]];
for(int j=0;j<26;j++)
if(st[rk[i]].nxt[j])
sum[rk[i]]+=sum[st[rk[i]].nxt[j]];
// cout<<i<<" "<<sum[i]<<endl;
}
}
void solve(){
if(k>sum[1]){
printf("-1");
return;
}
int now=1;
k-=app[1];sum[1]=0;
while(k>0){
int p=0;
while(k>sum[st[now].nxt[p]]){
k-=sum[st[now].nxt[p]];
p++;
}
now=st[now].nxt[p];
printf("%c",'a'+p);
k-=app[now];
}
}
int main(){
scanf("%s",s+1);
int lens=strlen(s+1);
sa_init();
for(int i=1;i<=lens;i++)
sa_extend((int)(s[i]-'a'));
scanf("%d%d",&t,&k);
work();
solve();
return 0;
}
[AHOI2013]差异
先说一句:为什么大家都是正向建的图啊?不是反向建,求两两前缀的最长公共后缀吗?
[反正我是倒着建的,而且过了]
题目说是求两两后缀的最长公共前缀,不久是后缀数组么。但后缀数组太难了,打不来,就只能用后缀自动机做了。
反向建后缀自动机,求成了两两前缀的最长公共后缀。
然而
是可以转化为一个定值的。
那么如何求两两前缀的最长公共后缀呢?
提示一下:
-
l
i
n
k
link
link树上两个点的
l
c
a
lca
lca就是这两个子串的最长公共后缀。
[证明显而易见,每个点的 f a t h e r father father都是自己的后缀]
对于一个点,我们把它当做
l
c
a
lca
lca,算出他的贡献就好了。
把它的子树两两的
s
i
z
e
size
size乘一下再乘上自己就是贡献了,注意别重复。
当然不必这么麻烦,思想还是一样,代码可以优化一下,详见代码哦。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int read(){
int x=0,f=1;char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
return f==1?x:-x;
}
const int N=7e5+5;
struct node{
int link,len,nxt[30],f;
}st[N<<1];
int siz,last;
void sa_init(){
siz=last=1;
st[1].len=0;
st[1].link=0;
}
void sa_extend(int c){
int cur=++siz;int p;
st[cur].len=st[last].len+1;
st[cur].f=1;
for(p=last;p&&!st[p].nxt[c];p=st[p].link)
st[p].nxt[c]=cur;
if(p==0)st[cur].link=1;
else{
int q=st[p].nxt[c];
if(st[q].len==st[p].len+1)st[cur].link=q;
else{
int clone=++siz;
st[clone].link=st[q].link;
for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];
st[clone].len=st[p].len+1;
for(;p&&st[p].nxt[c]==q;p=st[p].link)
st[p].nxt[c]=clone;
st[q].link=st[cur].link=clone;
}
}
last=cur;
}
int lens;
int c[N<<1],rk[N<<1];
char s[N];
ll count(){
for(int i=1;i<=siz;i++)c[st[i].len]++;
for(int i=1;i<=lens;i++)c[i]+=c[i-1];//
for(int i=1;i<=siz;i++)rk[c[st[i].len]--]=i;//排在第几位的是i
ll ans=0;
for(int i=siz;i>0;i--){
int x=rk[i];
ans+=(ll)st[x].f*st[st[x].link].f*st[st[x].link].len;
//现在父亲存储的大小还没有算自己,也就是自己兄弟的大小和
st[st[x].link].f+=st[x].f;
}
return ans;
}
int main(){
scanf("%s",s+1);
lens=strlen(s+1);
sa_init();
for(int i=lens;i>0;i--)
sa_extend(s[i]-'a');
printf("%lld",(ll)(lens-1)*lens*(lens+1)/2-2*count());
return 0;
}
[APIO2014]回文串
说实话,这道题做完了,还是好懵B。
还是一样的,先建后缀自动机,再算一算每一个子串出现的次数。然后再把反串放在后缀自动机跳跳就好了。操作就看代码吧。[有注释的]
推荐:题解
虽然图片挂了,就将就吧……
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
#define ll long long
struct node{
int link,len,nxt[28],maxend;
}st[N<<1];;
int cnt,last;
void sa_init(){
cnt=last=1;
st[1].link=st[1].len=0;
}
int f[N<<1];
void sa_extend(int c,int pos){
int cur=++cnt;int p;
st[cur].len=st[last].len+1;
st[cur].maxend=pos;
f[cur]=1;
for(p=last;p&&!st[p].nxt[c];p=st[p].link)//
st[p].nxt[c]=cur;
if(!p)st[cur].link=1;
else{
int q=st[p].nxt[c];
if(st[q].len==st[p].len+1)st[cur].link=q;
else{
int clone=++cnt;
st[clone].link=st[q].link;
st[clone].maxend=st[q].maxend;
for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];
st[clone].len=st[p].len+1;
for(;p&&st[p].nxt[c]==q;p=st[p].link)
st[p].nxt[c]=clone;
st[q].link=st[cur].link=clone;
}
}
last=cur;
}
char s[N];
int c[N],lens,rk[N<<1],vis[N<<1];//
ll ans=0;
void work(){//反串在后缀自动机上跑
int now=1,l=1;
for(int i=lens;i>0;i--){
while(now>1&&!st[now].nxt[s[i]-'a']){//往上跳,找到一个有这个字符转移的[找回文边缘]
now=st[now].link;
l=st[now].len;
}
if(st[now].nxt[s[i]-'a']){
now=st[now].nxt[s[i]-'a'];//转移
l++;
}
if(st[now].maxend<i+l){//这个串在maxendpos右边
//如果是左边就没有贡献 ———————————————————
//其他的endpos都没有maxendpos优 ——————————————————
if(i<=st[now].maxend)//这个串与maxendpos有交集
ans=max(ans,(ll)(st[now].maxend-i+1)*f[now]);
for(int p=st[now].link;p&&!vis[p];p=st[p].link){
vis[p]=1;//打标记,节省时间
if(i<=st[p].maxend&&st[p].maxend<=i+st[p].len-1)//有交集
ans=max(ans,(ll)(st[p].maxend-i+1)*f[p]);
}
}
}
}
int main(){
scanf("%s",s+1);
lens=strlen(s+1);
sa_init();
for(int i=1;i<=lens;i++)
sa_extend(s[i]-'a',i);
for(int i=1;i<=cnt;i++)c[st[i].len]++;
for(int i=1;i<=lens;i++)c[i]+=c[i-1];
for(int i=1;i<=cnt;i++)rk[c[st[i].len]--]=i;
for(int i=cnt;i>0;i--){
int x=rk[i];
st[st[x].link].maxend=max(st[st[x].link].maxend,st[x].maxend);
f[st[x].link]+=f[x];
}
work();
printf("%lld",ans);
return 0;
}