后缀自动机
具体构造方法和代码解析:史上最通俗的后缀自动机详解
后缀自动机SAM的定义
- 从源节点开始走,走到任何一个节点的路径上的字符连起来都是字符串的子串,有限,各不相同
- 子串都可以从源节点做到节点上
- 不是子串的字符串无法从源节点走到其中的节点
- 其中,所有源节点到终止节点的路径上的字符均为后缀
\(endposs\)(一节点两种含义)
定义:\(endposs(p)\)表示子串\(p\)出现的位置的右端点组成的集合,\(fa\)表示parent树上的父亲节点
性质:
- \(|endposs|\)为子串个数
- 如果两子串\(endposs(p)\)相同,则其中一个为另一个的后缀
- 如果\(p,q(len(p)<len(q))\) 要么\(endposs(q)\subset endposs(p)\),要么\(endposs(p)\cap endposs(q)=\emptyset\)
- 可以将\(endposs\)相同的归于一个\(endposs\)等价类;将等价类中元素按长度排序后,长度是连续的,每一个都是上一个的后缀
- 所有\(endposs等价类\)中的字符串不重也不漏,表示了所有子串
- \(endposs\)等价类个数是\(O(n)\)
由性质4和5可以发现,\(endposs\)等价类的子串集合是很好的后缀自动机的节点选择,压缩了子串的信息
通常题目中所需要的性质往往和maxlen有关
祖先树 the parent tree,又称为后缀树
用\(endposs\)集合作为节点
源点是空串,即\({1,2,3,...,n}\)
从源点在parent tree上沿边走,相当于一直往前加字母,\(endposs\)也在这过程中不断被儿子们完全分割
可以发现:\(p\)和\(fa\)等价类中的子串拼在一起是连续的
\(len\)表示等价类中最大子串的长度,\(minlen\)表示最短子串的长度
其中\(|endposs|\)可以利用一个集合会被分割成大小为1的集合DP求出(不一定要把树建出来,还可以根据len基数排序后倒序统计)
再者可以可持久化的线段树合并统计
通常后缀自动机应用的是后缀树,求子串的方式也往往是前缀的后缀
后缀自动机的构造
- 应用\(endposs\)的等价类中的子串集合作为节点(公用节点)
- 源点是空串,原来的源点,终点是原串所属于的节点及其在parent tree上的祖先
- 连边使得源点出发到点\(i\)的任意路径形成的字符串均属于\(i\)的\(endposs\)等价类子串集合,且属于\(i\)的\(endposs\)等价类子串集合等价于源点出发到点\(i\)的任意路径形成的字符串
点数最大为2n,边数最大为3n,是一张DAG
模板
(题链)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+5;
int n,ex[N],tot,lst;
char s[N];
struct A{
int fa,len; int c[123];
}dian[N<<1];
inline void SAM(int ch) {
int np=++tot,p=lst; lst=np;
dian[np].len=dian[p].len+1;
for(;p&&!dian[p].c[ch];p=dian[p].fa) dian[p].c[ch]=np;
if(!p) dian[np].fa=1;
else {
int q=dian[p].c[ch];
if(dian[p].len+1==dian[q].len) dian[np].fa=q;
else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
}
}
}
int main(){
scanf("%d%s",&n,s+1);
tot=lst=1;
for(int i=1;i<=n;i++) SAM(s[i]);
ll ans=0;
for(int i=1;i<=tot;i++) {
ans+=dian[i].len-dian[dian[i].fa].len;
}
printf("%lld\n",ans);
return 0;
}
PS:注意源点为1,lst=1
例题2:
luogu P3975 [TJOI2015]弦论
由子串想到后缀自动机
在SAM图上跑,每次从小到大选转移边,之后的所有子串数(不包括自身)是否在K内
不妨设\(f[i]\)表示子串数,\(g[i]\)表示字符串\(i\)点的所有位置,即\(endposs\)集合大小
两遍拓扑排序分别求\(f,g\)
PS:别算上\(1\)号点,是空串
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+5;
int n,op,K,tot,lst,q[N],g[N],d[N],l,r;
char s[N];
vector<int>V[N];
ll f[N];
struct A{
int fa,len; int c[26];
}dian[N];
inline void SAM(int ch) {
int np=++tot,p=lst; lst=np;
dian[np].len=dian[p].len+1; g[np]=1;
for(;p&&!dian[p].c[ch];p=dian[p].fa) dian[p].c[ch]=np;
if(!p) dian[np].fa=1;
else {
int q=dian[p].c[ch];
if(dian[p].len+1==dian[q].len) dian[np].fa=q;
else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
}
}
}
void dfs(int u) {
for(int i=0;i<26;i++) {
int v=dian[u].c[i];
if(v) {
dfs(v);
f[u]+=f[v]+g[v];
}
}
}
void dgs(int u,int k) {
if(k<=g[u]) return;
k-=g[u];
for(int i=0;i<=25;i++) {
int v=dian[u].c[i];
if(v) {
if(k<=f[v]+g[v]) {
putchar(i+'a'),dgs(v,k);
break;
}
k-=f[v]+g[v];
}
}
}
int main(){
int op; scanf("%s%d%d",s+1,&op,&K); n=strlen(s+1);
tot=lst=1;
for(int i=1;i<=n;i++) SAM(s[i]-'a');
if(op) {
l=1,r=0;
for(int i=1;i<=tot;i++) {
d[dian[i].fa]++;
}
for(int i=1;i<=tot;i++) {
if(!d[i]) q[++r]=i;
}
while(l<=r) {
int u=q[l];l++;
d[dian[u].fa]--;
if(!d[dian[u].fa]) q[++r]=dian[u].fa;
g[dian[u].fa]+=g[u];
}
} else {
for(int i=1;i<=tot;i++) g[i]=1;
}
g[1]=0;
l=1,r=0;
for(int i=1;i<=tot;i++) {
for(int j=0;j<=25;j++) {
if(dian[i].c[j]) {
d[i]++,V[dian[i].c[j]].push_back(i);
}
}
if(d[i]==0) q[++r]=i;
}
while(l<=r) {
int u=q[l]; l++;
for(auto v:V[u]) {
d[v]--;
if(d[v]==0) q[++r]=v;
f[v]+=f[u]+g[u];
}
}
if(f[1]+g[1]<K) puts("-1");
else {
dgs(1,K);
}
return 0;
}
例题3:
Luogu P4248 [AHOI2013]差异
可以把\(\sum\)拆开,只需要计算\(\sum_{i<j}LCP(suf[i],suf[j])\)
这显然是后缀数组裸题,所以我们采用后缀自动机来解决
后缀的前缀很难看,不妨翻转字符串,求前缀的后缀,这显然是\(parent tree\)上的LCA
考虑是哪个节点,显然是最大的字符串
对每个节点求出下面的节点数量,分段计算LCA到上个LCA的距离即可
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+5;
int n,tot,lst,g[N],q[N],d[N];
ll ans;
char s[N];
struct A{
int len,fa,c[30];
}dian[N];
inline void SAM(int ch) {
int np=++tot,p=lst; lst=np;
dian[np].len=dian[p].len+1; g[np]=1;
for(;p&&!dian[p].c[ch];p=dian[p].fa) dian[p].c[ch]=np;
if(!p) dian[np].fa=1;
else {
int q=dian[p].c[ch];
if(dian[q].len==dian[p].len+1) dian[np].fa=q;
else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
}
}
}
int main(){
scanf("%s",s+1); n=strlen(s+1);
for(int i=1;i<=n/2;i++) swap(s[i],s[n-i+1]);
ans=(ll)(n-1)*n*(n+1)>>1;
tot=lst=1;
for(int i=1;i<=n;i++) SAM(s[i]-'a');
int l=1,r=0;
for(int i=1;i<=tot;i++) {
d[dian[i].fa]++;
}
for(int i=1;i<=tot;i++) {
if(!d[i]) q[++r]=i;
}
while(l<=r) {
int u=q[l];l++;
d[dian[u].fa]--;
if(!d[dian[u].fa]) q[++r]=dian[u].fa;
g[dian[u].fa]+=g[u];
}
for(int i=2;i<=tot;i++) {
ans-=(ll)g[i]*(g[i]-1)*(dian[i].len-dian[dian[i].fa].len);
}
printf("%lld\n",ans);
return 0;
}
例题4:
CF700E Cool Slogansybtoj
考虑对于一个满足条件的序列\((S_1,S_2,S_3,...,S_n)\),可以依次将\(S_{n-1},S_{n-2},S_{n-3},...\)消去末尾部分,使得\(S_i\)是\(S_{i-1}\)的后缀
显然新产生的序列\({S_1',S_2',S_3'...,S_n}\)同样符合要求
也就是说只要统计\(S_i\)均为\(S_{i-1}\)后缀的最大序列长度即可
联想到后缀树,即猜想将对每条链中的节点拿出来单独做(其中\(endposs\)相同的点是不可能存在双倍关系),显然要用一定用\(maxlen\)字符串(min想想都不靠谱)。
但有没有可能一个节点所代表的不同字符串A,B在其子树节点的\(maxlen\)C中有不同个数呢?
显然,为了DP的可行性,感觉没有,我们下面用反证法证明:不妨设\(A>B\),且\(A,B\)在\(C\)中子串数量不同,其中\(B\)出现次数大于1
由于\(A,B\)的\(endposs\)集合一样,\(B\)为\(A\)的后缀,将\(A\)中在\(B\)前面的字母加入\(C\)显然\(endposs\)集合也不变,所以C不再是最大,与假设不符,即证
所以考虑把链拿出来DP,不妨设\(f[i]\)表示\(i\)及之前的最长长度
发现1:发现如果\(f\)一样,则\(j\)越小越优
发现2:\(f\)单调不降,每次只加1
所以每次只需要考虑答案为\(f[i-1]\)的点,可以设\(g[i]\)表示答案为\(f[i]\)的结尾的最小序号
接下来考虑:如何判断是否子串数量,可以用\(endposs\)数组
显然子串数量一定>=1(后缀),只需要在祖先的\(endposs\)中查询是否有在\([R[i]-mx[i]+1+mx[fa]-1,R[i]-1]\)
\(R[i]\)表示随便一个\(endposs\)中的元素
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=4e5+5,M=1e7+5;
int n,cnt,lst,tot,a[N],num[N],ls[M],rs[M],f[N],g[N],rt[N],mx[N],pa[N],t[N];
char s[N],s1[N];
struct A{
int c[26];
}dian[N];
void bld(int &p,int l,int r,int x) {
if(!p) p=++cnt;
if(l==r) return;
int mid=l+r>>1;
if(x<=mid) bld(ls[p],l,mid,x);
else bld(rs[p],mid+1,r,x);
}
void SAM(int ch) {
int p=lst,np=++tot; lst=np;
num[np]=mx[np]=mx[p]+1,bld(rt[np],1,n,mx[np]);
for(;p&&!dian[p].c[ch];p=pa[p]) dian[p].c[ch]=np;
if(!p) pa[np]=1;
else {
int q=dian[p].c[ch];
if(mx[q]==mx[p]+1) pa[np]=q;
else {
int nq=++tot; dian[nq]=dian[q];
num[nq]=num[q],mx[nq]=mx[p]+1,pa[nq]=pa[q];
pa[q]=pa[np]=nq;
for(;p&&dian[p].c[ch]==q;p=pa[p]) dian[p].c[ch]=nq;
}
}
}
int ask(int p,int l,int r,int x,int y) {
if(!p) return 0;
if(x==l&&r==y) return 1;
int mid=l+r>>1;
if(y<=mid) return ask(ls[p],l,mid,x,y);
if(x>mid) return ask(rs[p],mid+1,r,x,y);
return ask(ls[p],l,mid,x,mid)|ask(rs[p],mid+1,r,mid+1,y);
}
int merge(int p,int p1) {
if(!p||!p1) return p|p1;
int np=++cnt;
ls[np]=merge(ls[p],ls[p1]);
rs[np]=merge(rs[p],rs[p1]);
return np;
}
int main(){
scanf("%d%s",&n,s+1); tot=lst=1;
for(int i=1;i<=n;i++) SAM(s[i]-'a');
for(int i=1;i<=tot;i++) t[mx[i]]++;
for(int i=1;i<=n;i++) t[i]+=t[i-1];
for(int i=tot;i;i--) a[t[mx[i]]--]=i;
for(int i=tot;i>1;i--) {
int u=a[i],fa=pa[u];
rt[fa]=merge(rt[fa],rt[u]);
}
int ans=1;
for(int i=2;i<=tot;i++) {
int u=a[i],fa=pa[u];
if(fa==1) {
f[u]=1; g[u]=u; continue;
}
int res=ask(rt[g[fa]],1,n,num[u]-mx[u]+mx[g[fa]],num[u]-1);
if(res) f[u]=f[fa]+1,g[u]=u,ans=max(ans,f[u]);
else f[u]=f[fa],g[u]=g[fa];
}
printf("%d\n",ans);
return 0;
}
结论:后缀树上祖先节点中的所有字符串在儿子节点的最大字符串中的子串数量一样
遍历
可以向遍历AC自动机一样遍历后缀自动机,没有出边就跳父亲(直到1号点,别跳了!!!),时间复杂度\(O(n)\)(\(n\)表示匹配串长度)
\(Pf:\)设\(len\)为匹配串的当前后缀匹配长度,因为每次跳父亲的时候子串长度都会减少,而每次长度至多加\(1\),所以至多为\(n\),即证
求出的是被当做匹配串位置\(i\)为结尾的最长相同后缀
例题5:
最长公共子串
暴力跑,注意维护\(len\)(因为一个节点表示多个字符串)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e5+5;
int n,op,tot,lst,ans,now,len;
char s[N];
struct A{
int fa,len; int c[26];
}dian[N];
inline void SAM(int ch) {
int np=++tot,p=lst; lst=np;
dian[np].len=dian[p].len+1;
for(;p&&!dian[p].c[ch];p=dian[p].fa) dian[p].c[ch]=np;
if(!p) dian[np].fa=1;
else {
int q=dian[p].c[ch];
if(dian[p].len+1==dian[q].len) dian[np].fa=q;
else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
}
}
}
inline void ask(int ch) {
while(now!=1&&!dian[now].c[ch]) {
now=dian[now].fa,len=dian[now].len;
}
if(dian[now].c[ch]) now=dian[now].c[ch],len++;
ans=max(ans,len);
}
int main(){
scanf("%s",s+1); n=strlen(s+1);
tot=lst=1;
for(int i=1;i<=n;i++) SAM(s[i]-'a');
scanf("%s",s+1),n=strlen(s+1);
now=1; len=0;
for(int i=1;i<=n;i++) ask(s[i]-'a');
printf("%d\n",ans);
return 0;
}
例题6
[NOI2018] 你的名字,ybtoj
考虑68分,\(l=1,r=n\)的情况,求多组\(S\)中的且不包括在\(T\)中的本质不同的子串
\(|T|\leq 5*10^5\)大得离谱,所以只能枚举\(S\),将\(S\)在\(T\)的SAM中遍历,求出所有每个点为结尾的最长的,设为\(a[i]\)
但不能和\(|T|\)有所挂钩,所以处理出再建出\(S\)的SAM,显然答案为$$\sum{i=1}^{tot} max(0,mx[i]-max(mx[fa],a[R[i]]))$$
其中\(R[i]\)为\(endposs\)中随便一个点
解释:当\(mx[i]<=a[R[i]]\),肯定不行;当\(mx[i]>a[R[i]]>=mx[fa]\),显然存在mx[i]-a[R[i]];当\(mx[fa]>a[R[i]]\),无影响,子串都是
考虑68->100:变化的之后\(a\)数组,所以考虑如何求出\([l,r]\)中的\(a\)
\(|T|\)的SAM中部分节点是完全属于\([l,r]\)的,部分是半属于的,必须保证遍历的时候时刻处在属于\([l,r]\)的部分
每次新到节点时判断一下,如果当前的\(endposs\)存在点\(\in [l-len+1,r]\)(len表示当前长度),则没关系
否则因为可能这个节点是半属于的,需要len一直慢慢减1,直到变为mx[fa],才退到父亲节点,这个过程只会出现\(O(n)\)次
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int rd() {
char ch=getchar(); int ret=0;
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') {
ret=(ret<<1)+(ret<<3)+ch-'0';
ch=getchar();
}
return ret;
}
const int N=2e6+5;
int n,tot,lst,mx[N],pa[N],R[N],c[N],b[N],a[N]; //2*5*4=40
char s[N];
struct A{int c[26]; }dian[N]; //240
inline void SAM(int ch) {
int p=lst,np=++tot; lst=np;
mx[np]=mx[p]+1; R[np]=mx[np];
for(;p&&!dian[p].c[ch];p=pa[p]) dian[p].c[ch]=np;
if(!p) pa[np]=1;
else {
int q=dian[p].c[ch];
if(mx[q]==mx[p]+1) pa[np]=q;
else {
int nq=++tot; dian[nq]=dian[q],mx[nq]=mx[p]+1,pa[nq]=pa[q];
pa[np]=pa[q]=nq;
for(;p&&dian[p].c[ch]==q;p=pa[p]) dian[p].c[ch]=nq;
}
}
}
namespace SAMS{ //300
const int N=1e6+5,M=2e7+5;
int n,cnt,mx[N],pa[N],tot,lst,ls[M],rs[M],a[N],c[N],rt[N];//4*(20+20+5)=180
struct A{int c[26]; }dian[N]; //4*30=120
inline void SAM(int ch) {
int p=lst,np=++tot; lst=np;
mx[np]=mx[p]+1;
for(;p&&!dian[p].c[ch];p=pa[p]) dian[p].c[ch]=np;
if(!p) pa[np]=1;
else {
int q=dian[p].c[ch];
if(mx[q]==mx[p]+1) pa[np]=q;
else {
int nq=++tot; dian[nq]=dian[q],mx[nq]=mx[p]+1,pa[nq]=pa[q];
pa[np]=pa[q]=nq;
for(;p&&dian[p].c[ch]==q;p=pa[p]) dian[p].c[ch]=nq;
}
}
}
void bld(int &p,int l,int r,int x) {
p=++cnt;
if(l==r) return;
int mid=l+r>>1;
if(x<=mid) bld(ls[p],l,mid,x);
else bld(rs[p],mid+1,r,x);
}
int merge(int x,int y) {
if(!x||!y) return x|y;
int z=++cnt;
ls[z]=merge(ls[x],ls[y]);
rs[z]=merge(rs[x],rs[y]);
return z;
}
bool ask(int p,int l,int r,int x,int y) {
if(x>y) return 0;
if(!p) return 0;
if(l==x&&r==y) return 1;
int mid=l+r>>1;
if(y<=mid) return ask(ls[p],l,mid,x,y);
if(x>mid) return ask(rs[p],mid+1,r,x,y);
return ask(ls[p],l,mid,x,mid)|ask(rs[p],mid+1,r,mid+1,y);
}
void main(int t){
tot=lst=1; n=t;
for(int i=1;i<=n;i++) {
SAM(s[i]-'a');
bld(rt[lst],1,n,i);
}
for(int i=1;i<=tot;i++) c[mx[i]]++;
for(int i=1;i<=n;i++) c[i]+=c[i-1];
for(int i=tot;i>1;i--) {
a[c[mx[i]]--]=i;
}
for(int i=tot;i>1;i--) {
int u=a[i]; rt[pa[u]]=merge(rt[pa[u]],rt[u]);
}
}
}
int main(){
// freopen("2.in","r",stdin);
scanf("%s",s+1); n=strlen(s+1);
SAMS::main(n); int T=rd();
while(T--) {
scanf("%s",s+1); n=strlen(s+1);
int l=rd(),r=rd();
tot=lst=1; int u=1,len=0;
for(int i=1;i<=n;i++) {
s[i]-='a'; SAM(s[i]);
while(u!=1&&!SAMS::dian[u].c[s[i]]) u=SAMS::pa[u],len=SAMS::mx[u];
if(SAMS::dian[u].c[s[i]]) u=SAMS::dian[u].c[s[i]],len++;
while(len&&!SAMS::ask(SAMS::rt[u],1,SAMS::n,l+len-1,r)) {
len--;
if(len==SAMS::mx[SAMS::pa[u]]) u=SAMS::pa[u];
}
a[i]=len;
}
//puts("");
for(int i=0;i<=n;i++) c[i]=0;
for(int i=1;i<=tot;i++) c[mx[i]]++;
for(int i=1;i<=n;i++) c[i]+=c[i-1];
for(int i=tot;i>1;i--){
b[c[mx[i]]--]=i;
}
for(int i=tot;i>1;i--) {
int u=b[i];
if(!R[pa[u]]) R[pa[u]]=R[u];
}
ll ans=0;
for(int i=2;i<=tot;i++) {
ans+=max(0,mx[i]-max(mx[pa[i]],a[R[i]]));
}
printf("%lld\n",ans);
for(int i=1;i<=tot;i++)R[i]=0;
for(int i=1;i<=tot;i++) {
for(int j=0;j<=25;j++) dian[i].c[j]=0;
}
}
return 0;
}
PS:基数排序时记得要么从2开始,要么把0清0
广义SAM
多个串插入,其他同样,\(endposs\)等价类依旧是独一无二的
处理的时候将:lst定为1,再改一下SAM中部分代码
注意到:如果遍历的时候有点了,就不需要新建节点了,必要的时候列个点,记得改lst,据说不然会挂
例题7:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+5;
int n,lst,tot;
char s[N];
struct A{
int len,fa,c[26];
}dian[N];
void SAM(int ch) {
if(dian[lst].c[ch]) {
int p=lst,q=dian[lst].c[ch];
if(dian[p].len+1==dian[q].len) {
lst=q; return;
} else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
lst=nq; return;
}
}
int p=lst,np=++tot; lst=np;
dian[np].len=dian[p].len+1;
for(;p&&!dian[p].c[ch];p=dian[p].fa) dian[p].c[ch]=np;
if(!p) dian[np].fa=1;
else {
int q=dian[p].c[ch];
if(dian[q].len==dian[p].len+1) dian[np].fa=q;
else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
}
}
}
int main(){
scanf("%d",&n);
tot=1;
for(int i=1;i<=n;i++) {
scanf("%s",s+1);int len=strlen(s+1); lst=1;
for(int j=1;j<=len;j++) SAM(s[j]-'a');
}
ll ans=0;
for(int i=1;i<=tot;i++) {
ans+=dian[i].len-dian[dian[i].fa].len;
}
printf("%lld\n",ans);
}
PS:记得赋tot的初值
例题8
广义后缀树
建出广义SAM后,依次再跑一边,可以用fl数组表示是否在i时访问,用Int数组记录(递增)
当然,也可以用栈记录
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+5;
int n,m,lst,tot,L[N],R[N],fl[N],a[N];
char s[N],s1[N];
struct A{
int len,fa,c[26];
}dian[N];
void SAM(int ch) {
if(dian[lst].c[ch]) {
int p=lst,q=dian[lst].c[ch];
if(dian[p].len+1==dian[q].len) {
lst=q; return;
} else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
lst=nq; return;
}
}
int p=lst,np=++tot; lst=np;
dian[np].len=dian[p].len+1;
for(;p&&!dian[p].c[ch];p=dian[p].fa) dian[p].c[ch]=np;
if(!p) dian[np].fa=1;
else {
int q=dian[p].c[ch];
if(dian[q].len==dian[p].len+1) dian[np].fa=q;
else {
int nq=++tot; dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].c[ch]==q;p=dian[p].fa) dian[p].c[ch]=nq;
}
}
}
int main(){
scanf("%d%d",&n,&m);
tot=1; int cnt=0;
for(int i=1;i<=n;i++) {
scanf("%s",s+1);int len=strlen(s+1); lst=1;
L[i]=cnt+1;
for(int j=1;j<=len;j++) {
SAM(s[j]-'a');
s1[++cnt]=s[j];
}
R[i]=cnt;
}
for(int i=1;i<=n;i++) {
int u=1;
for(int j=L[i];j<=R[i];j++) {
u=dian[u].c[s1[j]-'a'];
int t=u;
while(t&&fl[t]!=i) {
a[t]++; fl[t]=i; t=dian[t].fa;
}
}
}
for(int i=1;i<=m;i++) {
scanf("%s",s+1); int len=strlen(s+1),u=1;
for(int j=1;j<=len;j++) {
u=dian[u].c[s[j]-'a'];
}
printf("%d\n",a[u]);
}
return 0;
}
例题9
luogu P4022 [CTSC2012]熟悉的文章,ybtoj
显然二分答案,后面转化成序列分段求最大,可以用DP
\(f[i]\)表示前\(i\)的最大值
\([j+1..i]\in SAM\)等价于\(j>=i-a[i]\)
可以发现\(i-a[i]\)随着\(i\)单调递增,\(i-L+1\)也是,所以可以用单调队列优化
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+5;
int n,m,tot,len,pa[N],mx[N],lst,a[N],q[N],f[N];
char s[N];
struct A{int c[2]; }dian[N];
inline void SAM(int ch) {
if(dian[lst].c[ch]) {
int p=lst,q=dian[p].c[ch];
if(mx[q]==mx[p]+1) {
lst=q; return;
}
int nq=++tot;
dian[nq]=dian[q],pa[nq]=pa[q],mx[nq]=mx[p]+1;
pa[q]=nq;
for(;p&&dian[p].c[ch]==q;p=pa[p]) dian[p].c[ch]=nq;
}
int p=lst,np=++tot; lst=np;
mx[np]=mx[p]+1;
for(;p&&!dian[p].c[ch];p=pa[p]) dian[p].c[ch]=np;
if(!p) pa[np]=1;
else {
int q=dian[p].c[ch];
if(mx[q]==mx[p]+1) pa[np]=q;
else {
int nq=++tot;
dian[nq]=dian[q],pa[nq]=pa[q],mx[nq]=mx[p]+1;
pa[np]=pa[q]=nq;
for(;p&&dian[p].c[ch]==q;p=pa[p]) dian[p].c[ch]=nq;
}
}
}
inline bool check(int L) {
int l=1,r=0;
for(int i=1;i<=n;i++) {
if(i-L>=0) {
while(l<=r&&f[q[r]]-q[r]<=f[i-L]-(i-L)) r--;
q[++r]=i-L;
}
while(l<=r&&i-a[i]>q[l]) l++;
f[i]=f[i-1];
if(l<=r) f[i]=max(f[i],f[q[l]]+i-q[l]);
}
return f[n]*10>=n*9;
}
int main(){
scanf("%d%d",&m,&n); tot=1;
for(int i=1;i<=n;i++) {
scanf("%s",s+1); len=strlen(s+1);
lst=1;
for(int i=1;i<=len;i++) SAM(s[i]-'0');
}
for(int i=1;i<=m;i++) {
scanf("%s",s+1); n=strlen(s+1);
int u=1,len=0;
for(int i=1;i<=n;i++) {
while(u&&!dian[u].c[s[i]-'0']) u=pa[u],len=mx[u];
if(dian[u].c[s[i]-'0']) len++,u=dian[u].c[s[i]-'0'];
a[i]=len;
}
int l=1,r=n,mid,ans=0;
while(l<=r){
mid=l+r>>1;
if(check(mid)) ans=mid,l=mid+1;
else r=mid-1;
}
printf("%d\n",ans);
}
return 0;
}

浙公网安备 33010602011771号