后缀自动机\((SAM)\)是一个能解决许多字符串相关问题的有力的数据结构ios
它能够把一个字符串的全部子串都表示出来c++
并且从根出发的任意一条合法路径都是该串中的一个子串数组
要表示一个字符串全部的子串,最简单的方法是对于全部的子串建一棵字典树数据结构
可是这样作时间复杂度和空间复杂度都是 \(O(n^2)\) 的ui
所以考虑把一些没有用的节点和边去掉spa
定义 \(endpos(p)\) 为一个子串 \(p\) 出现的全部位置的右端点标号组成的集合code
关于 \(endpos\) 有以下的性质排序
\(1\)、若是两个子串的 \(endpos\) 相同,则其中子串一个必然为另外一个的后缀继承
\(2\)、对于任意两个子串 \(t\) 和 \(p\)\(( len_t\le len_p)\),要么\(endpos(t)\in endpos(p)\),要么 \(endpos(t) \bigcap endpos(p)=\emptyset\)队列
\(3\)、对于 \(endpos\) 相同的子串,咱们将它们归为一个 \(endpos\) 等价类
对于任意一个 \(endpos\) 等价类,将包含在其中的全部子串依长度从大到小排序,则每个子串的长度均为上一个子串的长度减 \(1\),且为上一个子串的后缀
简单来讲,一个 \(endpos\) 等价类内的串的长度连续
\(4\)、\(endpos\) 等价类个数的级别为 \(O(n)\)
若是咱们在一个字符串的前面添加两个不一样的字符
能够把当前的集合分红两个没有交集的集合
相似于线段树的分割方法能够达到最大的划分数 \(2n\)
因此后缀自动机的空间要开 \(2\) 倍
若是把母集做为分割出来的小集合的父亲,就会造成一棵树,这就是 \(parent\ tree\)
\(parent\ tree\) 上的节点还有一个性质 \(minlen[now]=maxlen[fa]+1\)
这样对于每个 \(endpos\) 等价类,只须要记录属于它的字符串的最大长度便可,最小长度能够由父亲节点推出来
后缀自动机是在 \(parent\) 树的基础上构建而来的,\(parent\) 树的节点就是后缀自动机的节点
固然,后缀自动机不只有 \(parent\ tree\) 的父子关系,也有 \(trie\) 树那样的转移边
沿着一个节点经过一条转移边到达另外一个节点表明着在当前字符串后面添加字符
从 \(parent\ tree\) 上的父亲节点达到儿子节点则表明着在当前字符串前面添加字符
具体构造的时候采用增量法构造
rg int p=lst; rg int np=lst=++cnt; len[np]=len[p]+1;
设当前已经构造到了字符串的第 \(n\) 个字符
首先记录一下添加第 \(n-1\) 个字符时新建的节点 \(p\)
对于当前的位置新开一个节点 \(np\),表明着只含有位置 \(n\) 的 \(endpos\) 集合
显然字符串 \(s[1 \cdots n]\) 在以前必定没有出现过,因此它必定属于 \(np\) 这个点
因此这个集合中最长的字符串是 \(s[1 \cdots n]\),也就是 \(s[1 \cdots n-1]+1\)
for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1;
如今咱们要考虑的就是 \(s[2 \cdots n],s[3 \cdots n] \cdots s[n \cdots n]\) 属于哪个 \(endpos\) 集合
由于咱们不知道它们在以前有没有出现过
因此去跳 \(np\) 在 \(parent\ tree\) 上的父亲
含义就是字符串 \(s[1 \cdots n-1]\) 在前面不断去掉字符
看 \(s[1 \cdots n-1],s[2 \cdots n-1] \cdots s[n-1 \cdots n-1]\) 能不能在后面添加一个字符 \(s[n]\)到达一个已有的状态
若是不能,就表明着当前长度以 \(n\) 结尾的字符串尚未出现过,那么它显然也属于 \(np\) 这个集合
同时由 \(p\) 向 \(np\) 建一条边,表明着能够在当前字符串的后面添加字符
若是都到根节点了尚未找到一个以前已经出现过的字符串
只能说明 \(s[n]\) 这个种类的字符以前没有出现
直接把它连向根节点就好了
rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q;
不然说明以 \(n\) 为结尾的子串以前出现过一部分
这些已经出现过的子串的 \(endpos\) 确定不是单独的一个 \(n\)
因此就不能把它归到 \(np\) 这个集合中了
并且咱们并不知道这些以 \(s[n]\) 结尾的子符串是否是 \(s[1 \cdots n]\) 的子串
有可能所有是,也有可能只有一部分是
若是所有是的话,咱们就不须要新开一个 \(endpos\) 集合了
只要在以前的 \(endpos\) 集合的基础上添一个 \(n\) 就好了
不然咱们就要把这两部分分开,一部分含有 \(n\),一部分不含有 \(n\)
判断的标准就是 \(len[q]=len[p]+1\)
由于咱们由 \(p\) 到 \(q\) 的含义就是在一个以 \(s[n-1]\) 结尾的字符串的后面加一个字符变成以 \(s[n]\) 结尾的字符串
若是出现了 \(q\) 中最长字符串不是仅仅添加了一个字符获得的状况
说明这个最长字符串必定不是以 \(s[n]\) 结尾的
反之亦然
若是知足条件就很好办了,直接把 \(np\) 的父亲设为 \(q\)
并且此时咱们不用去跳 \(p\) 的父亲了
由于 \(p\) 的父亲节点的出边指向的节点必定全是以 \(s[n]\) 结尾的
并且这些字符串必定是 \(q\) 中字符串的后缀
它们的 \(endpos\) 集合都总体加了 \(n\)
rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
若是不知足呢
确定要把两个集合分开
新建一个点 \(nq\) 存储 \(endpos\) 多了 \(n\) 的字符串
显然这些字符串中长度最长的就是 \(len[p]+1\)
因此 \(len[nq]=len[p]+1\)
由于咱们只是把原来的集合一分为二,因此原来集合的出边直接让两个儿子继承就好了
可是父子关系要变一下
由于 \(nq\) 和 \(q\) 是由一个集合分出来的,它们确定知足后缀关系
由于 \(nq\) 的长度更短而且 \(endpos\) 集合中的元素更多
因此要把 \(q\) 的父亲设为 \(nq\)
同理 \(np\) 的父亲也是 \(nq\)
最后再把本应该指向 \(nq\) 的边改过来就好了
void insert(rg int c){ rg int p=lst; rg int np=lst=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1; else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } siz[np]=1; }
这样建造出来的后缀自动机是一个\(DAG\)(\(AC\)自动机则是一个 \(Trie\) 图)
和回文自动机不一样,长度长的不必定编号大,也就是说 \(1 \sim n\) 不必定是一个拓扑序
因此还要按照长度进行桶排,获得的才是最终的拓扑序
和普通的后缀自动机同理
加入一个新的字符串以前,要把 \(lst\) 重置成 \(1\)
对于以前已经出现过的节点不要重复去建就好了
要注意的是广义后缀自动机不能按照桶排来肯定拓扑序
要用正常的队列的写法
struct SAM{ int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn]; int insert(rg int lst,rg int c){ rg int p=lst; if(ch[p][c]){ rg int q=ch[p][c]; if(len[q]==len[p]+1) return q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; return nq; } } rg int np=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1; else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } return np; } void build(){ cnt=1; n=read(); for(rg int i=1;i<=n;i++){ scanf("%s",s+1); rg int nlen=strlen(s+1); for(rg int lst=1,j=1;j<=nlen;j++){ lst=insert(lst,s[j]-'a'+1); siz[lst]++; } } } void calc(){ rg long long ans=0; for(rg int i=1;i<=cnt;i++){ ans+=len[i]-len[fa[i]]; } printf("%lld\n",ans); } }sam;
广义后缀自动机的模板题
对于节点 \(i\),它表明的 \(endpos\) 集合中本质不一样的字符串一共有 \(len[i]-len[fa[i]]\) 个
对于两个字符串,分别记录一下它们在某个 \(endpos\) 集合中出现的次数
最终的答案就是 \((len[i]-len[fa[i]]) \times siz[i][0] \times siz[i][1]\)
#include<cstdio> #include<cstring> #include<algorithm> #include<queue> #define rg register inline int read(){ rg int x=0,fh=1; rg char ch=getchar(); while(ch<'0' || ch>'9'){ if(ch=='-') fh=-1; ch=getchar(); } while(ch>='0' && ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*fh; } const int maxn=2e6+5; char s[maxn]; struct SAM{ int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn][2],rd[maxn]; std::queue<int> q; int insert(rg int lst,rg int c){ rg int p=lst; if(ch[p][c]){ rg int q=ch[p][c]; if(len[q]==len[p]+1) return q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; return nq; } } rg int np=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1; else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } return np; } void build(){ cnt=1; for(rg int i=0;i<=1;i++){ scanf("%s",s+1); rg int nlen=strlen(s+1); for(rg int lst=1,j=1;j<=nlen;j++){ lst=insert(lst,s[j]-'a'+1); siz[lst][i]++; } } for(rg int i=1;i<=cnt;i++) rd[fa[i]]++; for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i); while(!q.empty()){ rg int now=q.front(); q.pop(); siz[fa[now]][0]+=siz[now][0],siz[fa[now]][1]+=siz[now][1]; --rd[fa[now]]; if(rd[fa[now]]==0) q.push(fa[now]); } } void calc(){ rg long long ans=0; for(rg int i=1;i<=cnt;i++){ ans+=1LL*(len[i]-len[fa[i]])*siz[i][0]*siz[i][1]; } printf("%lld\n",ans); } }sam; int main(){ sam.build(); sam.calc(); return 0; }
用线段树合并维护每个 \(emdpos\) 集合中有多少个字符串出现过
若是只有一种字符串出现过就累加答案
注意有些状况下后缀自动机上的线段树合并和普通的线段树合并有所不一样
普通的线段树合并会破坏原来两颗线段树的形态,想要查询以前的信息就不许确了
因此合并的时候要新开一个节点
这道题由于是边查询边统计答案,因此用正常的写法就行
#include<cstdio> #include<cstring> #include<iostream> #define rg register inline int read(){ rg int x=0,fh=1; rg char ch=getchar(); while(ch<'0' || ch>'9'){ if(ch=='-') fh=-1; ch=getchar(); } while(ch>='0' && ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*fh; } const int maxn=1e6+5; struct trr{ int lch,rch,siz; }tr[maxn*20]; int rt[maxn],cnt,n; void push_up(rg int da){ tr[da].siz=tr[tr[da].lch].siz+tr[tr[da].rch].siz; } int ad(rg int da,rg int l,rg int r,rg int wz){ if(!da) da=++cnt; if(l==r){ tr[da].siz=1; return da; } rg int mids=(l+r)>>1; if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz); else tr[da].rch=ad(tr[da].rch,mids+1,r,wz); push_up(da); return da; } int bing(rg int aa,rg int bb,rg int l,rg int r){ if(!aa || !bb) return aa+bb; if(l==r){ tr[aa].siz+=tr[bb].siz; tr[aa].siz=1; return aa; } rg int mids=(l+r)>>1; tr[aa].lch=bing(tr[aa].lch,tr[bb].lch,l,mids); tr[aa].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r); push_up(aa); return aa; } int cx(rg int da,rg int l,rg int r){ if(l==r) return l; rg int mids=(l+r)>>1; if(tr[tr[da].lch].siz) return cx(tr[da].lch,l,mids); else return cx(tr[da].rch,mids+1,r); } char s[maxn]; struct SAM{ int ch[maxn][28],fa[maxn],cnt,len[maxn],tax[maxn],a[maxn],ans[maxn]; int insert(rg int lst,rg int c){ rg int p=lst; if(ch[p][c]){ rg int q=ch[p][c]; if(len[q]==len[p]+1) return q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; return nq; } } rg int np=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p){ fa[np]=1; } else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } return np; } void build(){ cnt=1; n=read(); for(rg int i=1;i<=n;i++){ scanf("%s",s+1); rg int nlen=strlen(s+1); for(rg int j=1,lst=1;j<=nlen;j++){ lst=insert(lst,s[j]-'a'+1); rt[lst]=ad(rt[lst],1,n,i); } } } void calc(){ for(rg int i=1;i<=cnt;i++) tax[len[i]]++; for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1]; for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i; for(rg int i=cnt;i>=1;i--){ rg int p=a[i]; if(tr[rt[p]].siz==1){ ans[cx(rt[p],1,n)]+=len[p]-len[fa[p]]; } rt[fa[p]]=bing(rt[fa[p]],rt[p],1,n); } for(rg int i=1;i<=n;i++){ printf("%d\n",ans[i]); } } }sam; int main(){ sam.build(); sam.calc(); return 0; }
给你一个字符串 \(S\), 有不少组询问, 每次给定一个 \(T\), 求 \(T\) 中不在 \(S[l:r]\) 中出现的本质不一样的子串个数
用 \(T\) 的本质不一样的子串减去 \(T\) 在 \(S[l:r]\) 中出现的本质不一样的子串个数
前者很好求,对于 \(T\) 建出后缀自动机,答案就是 \(\sum len[i]-len[fa[i]]\)
关键在于如何求出后面的部分
先考虑最简单的匹配问题
即对于一个字符串 \(T\),求出它与另外一个字符串 \(S\) 的最长公共子串
对于 \(S\) 创建后缀自动机,把初始的位置置为根节点
对于 \(T\) 从第一个字符开始枚举
若是后缀自动机的节点上有当前字符的出边就一直走下去,同时把匹配长度加一
不然就一直跳 \(parent\ tree\) 直到匹配上为止,把匹配长度置为当前节点的长度
若是到了根节点还匹配不上,就把匹配长度置为 \(0\)
now=1,cs=0; for(rg int i=1;i<=n;i++){ rg int p=s[i]-'a'+1; while(now && !ch[now][p]) now=fa[now],cs=len[now]; if(!now){ now=1; cs=0; } else { cs++; now=ch[now][p]; } ans=std::max(ans,cs); }
如今无非是在普通匹配的基础上加上了 \([l,r]\) 的限制
只要用线段树合并维护一下 \(endpos\) 集合便可,这里要写新开节点的那一种
在线段树上查询当前的节点在 \([l,r]\) 中能匹配的最靠右的端点
若是一直不存在就一直向上跳
跳到第一个合法的位置以后还要继续向上跳
由于越往上 \(endpos\) 集合中含有的元素越多
查询右端点时获得的答案也就越靠右
一直跳到当前节点的长度限制答案为止
还有一个问题就是如何去重
由于不一样位置本质相同的字符串只算一次
这时候咱们就须要对于 \(T\) 建一个后缀自动机
这样就能够求出以每个节点为结尾的第一次出现的字符串的个数
每一位匹配的长度和这个值取一个 \(min\) 便可
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #define rg register const int maxn=1e6+5; char s[maxn]; int n,t,rt[maxn],trcnt,l,r; struct trr{ int lch,rch,mmax; }tr[maxn*20]; void push_up(rg int da){ tr[da].mmax=std::max(tr[tr[da].lch].mmax,tr[tr[da].rch].mmax); } int ad(rg int da,rg int l,rg int r,rg int wz){ da=++trcnt; if(l==r){ tr[da].mmax=wz; return da; } rg int mids=(l+r)>>1; if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz); else tr[da].rch=ad(tr[da].rch,mids+1,r,wz); push_up(da); return da; } int bing(rg int aa,rg int bb,rg int l,rg int r){ if(!aa || !bb) return aa+bb; rg int cc=++trcnt,mids=(l+r)>>1; if(l==r){ tr[cc].mmax=std::max(tr[aa].mmax,tr[bb].mmax); return cc; } tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids); tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r); push_up(cc); return cc; } int cx(rg int da,rg int l,rg int r,rg int L,rg int R){ if(!da) return -1; if(l>=L && r<=R) return tr[da].mmax; rg int nans=-1,mids=(l+r)>>1; if(L<=mids) nans=std::max(nans,cx(tr[da].lch,l,mids,L,R)); if(R>mids) nans=std::max(nans,cx(tr[da].rch,mids+1,r,L,R)); return nans; } struct SAM{ int len[maxn],ch[maxn][28],fa[maxn],lst,cnt,id[maxn],tax[maxn],nlen,jl[maxn]; void insert(rg int c){ rg int p=lst; rg int np=lst=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1; else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } } void build(rg int op){ for(rg int i=1;i<=cnt;i++) fa[i]=0,memset(ch[i],0,sizeof(ch[i])); lst=cnt=1; nlen=strlen(s+1); for(rg int i=1;i<=nlen;i++){ insert(s[i]-'a'+1); jl[i]=len[fa[lst]]; if(!op) rt[lst]=ad(rt[lst],1,n,i); } if(!op){ for(rg int i=1;i<=cnt;i++) tax[len[i]]++; for(rg int i=1;i<=nlen;i++) tax[i]+=tax[i-1]; for(rg int i=1;i<=cnt;i++) id[tax[len[i]]--]=i; for(rg int i=cnt;i>=1;i--){ rg int tmp=id[i]; if(fa[tmp]) rt[fa[tmp]]=bing(rt[fa[tmp]],rt[tmp],1,n); } } } }sam1,sam2; long long solve(rg int l,rg int r){ rg long long ans=0; for(rg int i=1;i<=sam2.cnt;i++) ans+=sam2.len[i]-sam2.len[sam2.fa[i]]; rg int now=1,cs=0,tmp,nrt,nans=0; for(rg int i=1;i<=sam2.nlen;i++){ rg int p=s[i]-'a'+1; tmp=cx(rt[sam1.ch[now][p]],1,n,l,r); while(now && tmp==-1){ now=sam1.fa[now],cs=sam1.len[now]; tmp=cx(rt[sam1.ch[now][p]],1,n,l,r); } if(!now) now=1,cs=0; else { now=sam1.ch[now][p],cs++,nrt=sam1.fa[now],nans=std::min(sam1.len[now],tmp-l+1); while(nrt){ tmp=cx(rt[nrt],1,n,l,r); nans=std::max(nans,std::min(tmp-l+1,sam1.len[nrt])); if(tmp-l+1>=sam1.len[nrt]) break; nrt=sam1.fa[nrt]; } nans=std::min(nans,cs); if(nans>sam2.jl[i]) ans-=(nans-sam2.jl[i]); } } return ans; } int main(){ scanf("%s",s+1); n=strlen(s+1); sam1.build(0); scanf("%d",&t); rg int l,r; for(rg int i=1;i<=t;i++){ scanf("%s%d%d",s+1,&l,&r); sam2.build(1); printf("%lld\n",solve(l,r)); } return 0; }
对于字符串数组创建广义后缀自动机
查询以前先对于串 \(S\) 在后缀自动上跑一遍匹配
对于每个位置记录以该位置结尾的后缀在后缀自动机上可以匹配的最长的位置以及对应的节点
对于每个 \(endpos\) 集合用线段树记录一下它在哪些子串中出现过以及出现的次数
若是要查询 \(s[l,r]\) 在字符串组中的那一个字符串中出现的次数最多
从预处理出来的以 \(r\) 结尾的后缀可以匹配的最长的位置对应的节点进行树上倍增
找到第一个长度大于等于 \(r-l+1\) 的位置
此时这个位置所含有的字符串的种类必定是最多的
直接在对应的线段树上查询便可
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> #include<queue> #define rg register inline int read(){ rg int x=0,fh=1; rg char ch=getchar(); while(ch<'0' || ch>'9'){ if(ch=='-') fh=-1; ch=getchar(); } while(ch>='0' && ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*fh; } const int maxn=1e6+5; struct trr{ int lch,rch,val,jl; }tr[maxn*20]; int trcnt,rt[maxn]; void push_up(rg int da){ if(tr[tr[da].lch].val<tr[tr[da].rch].val) tr[da].jl=tr[tr[da].rch].jl; else if(tr[tr[da].rch].val<tr[tr[da].lch].val) tr[da].jl=tr[tr[da].lch].jl; else tr[da].jl=std::min(tr[tr[da].lch].jl,tr[tr[da].rch].jl); tr[da].val=std::max(tr[tr[da].lch].val,tr[tr[da].rch].val); } int ad(rg int da,rg int l,rg int r,rg int wz){ if(!da) da=++trcnt; if(l==r){ tr[da].val++; tr[da].jl=wz; return da; } rg int mids=(l+r)>>1; if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz); else tr[da].rch=ad(tr[da].rch,mids+1,r,wz); push_up(da); return da; } int bing(rg int aa,rg int bb,rg int l,rg int r){ if(!aa || !bb) return aa+bb; rg int cc=++trcnt,mids=(l+r)>>1; if(l==r){ tr[cc].val=tr[aa].val+tr[bb].val; tr[cc].jl=l; return cc; } tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids); tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r); push_up(cc); return cc; } int cx(rg int da,rg int l,rg int r,rg int L,rg int R){ if(!da) return 0; if(l>=L && r<=R) return da; rg int mids=(l+r)>>1,nans=0,tmp; if(L<=mids){ tmp=cx(tr[da].lch,l,mids,L,R); if(tr[tmp].val>tr[nans].val) nans=tmp; else if(tr[tmp].val==tr[nans].val){ if(tr[tmp].jl<tr[nans].jl) nans=tmp; } } if(R>mids){ tmp=cx(tr[da].rch,mids+1,r,L,R); if(tr[tmp].val>tr[nans].val) nans=tmp; else if(tr[tmp].val==tr[nans].val){ if(tr[tmp].jl<tr[nans].jl) nans=tmp; } } return nans; } char s1[maxn],s2[maxn]; int n,m,q,mat[maxn]; struct SAM{ int fa[maxn],ch[maxn][28],len[maxn],cnt,nlen,rd[maxn],zx[maxn][22],sta[maxn],tp,mmax[maxn]; std::queue<int> q; int insert(rg int lst,rg int c){ rg int p=lst; if(ch[p][c]){ rg int q=ch[p][c]; if(len[q]==len[p]+1) return q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; return nq; } } rg int np=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1; else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } return np; } void build(){ cnt=1; for(rg int i=1;i<=m;i++){ scanf("%s",s2+1); nlen=strlen(s2+1); for(rg int j=1,lst=1;j<=nlen;j++){ lst=insert(lst,s2[j]-'a'+1); rt[lst]=ad(rt[lst],1,m,i); } } for(rg int i=1;i<=cnt;i++) rd[fa[i]]++,zx[i][0]=fa[i]; for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i); while(!q.empty()){ rg int now=q.front(); q.pop(); sta[++tp]=now; rt[fa[now]]=bing(rt[fa[now]],rt[now],1,m); rd[fa[now]]--; if(rd[fa[now]]==0) q.push(fa[now]); } for(rg int i=tp;i>=1;i--){ rg int now=sta[i]; for(rg int j=1;j<=20;j++){ zx[now][j]=zx[zx[now][j-1]][j-1]; } } } void pre(){ rg int cs=0,now=1; for(rg int i=1;i<=n;i++){ rg int p=s1[i]-'a'+1; while(now && !ch[now][p]) now=fa[now],cs=len[now]; if(!now) now=1,cs=0; else now=ch[now][p],cs++; mat[i]=now,mmax[i]=cs; } } void solve(rg int l1,rg int r1,rg int l2,rg int r2){ if(mmax[r2]<r2-l2+1){ printf("%d 0\n",l1); return; } rg int now=mat[r2]; for(rg int i=20;i>=0;i--){ if(len[zx[now][i]]>=r2-l2+1) now=zx[now][i]; } if(len[now]<r2-l2+1){ printf("%d 0\n",l1); return; } rg int tmp=cx(rt[now],1,m,l1,r1); if(!tmp) printf("%d 0\n",l1); else printf("%d %d\n",tr[tmp].jl,tr[tmp].val); } }sam; int main(){ scanf("%s",s1+1); n=strlen(s1+1); m=read(); sam.build(),sam.pre(); q=read(); rg int l1,r1,l2,r2; for(rg int i=1;i<=q;i++){ l1=read(),r1=read(),l2=read(),r2=read(); sam.solve(l1,r1,l2,r2); } return 0; }
一个字符串出现的次数就是它子树内的权值和
强制在线要用 \(lct\) 维护
由于子树和很差维护
因此能够在每一次修改后把当前节点到根的路径都加上对应的权值
查询的时候只要单点查询就好了
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define rg register inline int read(){ rg int x=0,fh=1; rg char ch=getchar(); while(ch<'0' || ch>'9'){ if(ch=='-') fh=-1; ch=getchar(); } while(ch>='0' && ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*fh; } const int maxn=3e6+5; char s[maxn]; std::string chars; int mask; void getit(int mask){ scanf("%s",s); chars=s; for(int j=0;j<chars.length();j++){ mask=(mask*131+j)%chars.length(); char t=chars[j]; chars[j]=chars[mask]; chars[mask]=t; } } struct LCT{ int ch[maxn][2],fa[maxn],sum[maxn],tag[maxn],rev[maxn],sta[maxn],tp; void push_down(rg int da){ rg int lc=ch[da][0],rc=ch[da][1]; if(rev[da]){ rev[lc]^=1,rev[rc]^=1,rev[da]^=1; std::swap(ch[da][0],ch[da][1]); } if(tag[da]){ if(lc){ tag[lc]+=tag[da]; sum[lc]+=tag[da]; } if(rc){ tag[rc]+=tag[da]; sum[rc]+=tag[da]; } tag[da]=0; } } bool isroot(rg int da){ return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da); } void xuanzh(rg int x){ rg int y=fa[x]; rg int z=fa[y]; rg int k=(ch[y][1]==x); if(!isroot(y)){ ch[z][ch[z][1]==y]=x; } fa[x]=z; ch[y][k]=ch[x][k^1]; fa[ch[x][k^1]]=y; ch[x][k^1]=y; fa[y]=x; } void splay(rg int x){ sta[tp=1]=x; for(rg int i=x;!isroot(i);i=fa[i]) sta[++tp]=fa[i]; for(rg int i=tp;i>=1;i--) push_down(sta[i]); while(!isroot(x)){ rg int y=fa[x]; rg int z=fa[y]; if(!isroot(y)){ (ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y); } xuanzh(x); } } void access(rg int x){ for(rg int y=0;x;y=x,x=fa[x]){ splay(x); ch[x][1]=y; } } void makeroot(rg int x){ access(x); splay(x); rev[x]^=1; push_down(x); } int findroot(rg int x){ access(x); splay(x); push_down(x); while(ch[x][0]){ x=ch[x][0]; push_down(x); } splay(x); return x; } void split(rg int x,rg int y){ makeroot(x); access(y); splay(y); } void link(rg int x,rg int y){ makeroot(x); if(findroot(y)!=x) fa[x]=y; } void cut(rg int x,rg int y){ makeroot(x); if(findroot(y)==x && fa[y]==x && ch[y][0]==0){ ch[x][1]=fa[y]=0; } } }lct; struct SAM{ int ch[maxn][28],fa[maxn],lst,len[maxn],cnt; void insert(rg int c){ rg int p=lst; rg int np=lst=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p){ fa[np]=1; lct.link(np,fa[np]); } else { rg int q=ch[p][c]; if(len[q]==len[p]+1){ fa[np]=q; lct.link(np,fa[np]); } else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); lct.cut(q,fa[q]); lct.link(nq,fa[q]); lct.link(np,nq); lct.link(q,nq); fa[nq]=fa[q]; fa[q]=fa[np]=nq; lct.splay(q); lct.sum[nq]=lct.sum[q]; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } lct.split(np,1); lct.sum[1]++; lct.tag[1]++; } void build(){ lst=cnt=1; scanf("%s",s+1); rg int nlen=strlen(s+1); for(int i=1;i<=nlen;i++){ insert(s[i]-'A'); } } void ad(){ rg int nlen=chars.length(); for(rg int i=0;i<nlen;i++) insert(chars[i]-'A'); } int cx(){ rg int now=1,nlen=chars.length(); for(rg int i=0;i<nlen;i++){ rg int p=chars[i]-'A'; if(!ch[now][p]) return 0; now=ch[now][p]; } lct.splay(now); return lct.sum[now]; } }sam; int q; int main(){ q=read(); sam.build(); for(rg int i=1;i<=q;i++){ scanf("%s",s+1); if(s[1]=='A'){ getit(mask); sam.ad(); } else { getit(mask); rg int nans=sam.cx(); printf("%d\n",nans); mask^=nans; } } return 0; }
在后缀自动机中两个字符串中的 \(lcp\) 就是它们在 \(fail\) 树上的最近公共祖先
题目给出的式子其实就是两两之间的路径长度
枚举每一条边的贡献加起来便可
#include<cstdio> #include<cstring> #include<algorithm> #define rg register const int maxn=4e6+5; char s[maxn]; long long ans; struct SAM{ int ch[maxn][28],fa[maxn],lst,cnt,len[maxn],a[maxn],tax[maxn],siz[maxn],n; void insert(rg int c){ rg int p=lst; rg int np=lst=++cnt; len[np]=len[p]+1; for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np; if(!p) fa[np]=1; else { rg int q=ch[p][c]; if(len[q]==len[p]+1) fa[np]=q; else { rg int nq=++cnt; len[nq]=len[p]+1; memcpy(ch[nq],ch[q],sizeof(ch[q])); fa[nq]=fa[q]; fa[q]=fa[np]=nq; for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; } } siz[np]=1; } void build(){ scanf("%s",s+1); n=strlen(s+1); std::reverse(s+1,s+1+n); lst=cnt=1; for(rg int i=1;i<=n;i++) insert(s[i]-'a'+1); } void calc(){ for(rg int i=1;i<=cnt;i++) tax[len[i]]++; for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1]; for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i; for(rg int i=cnt;i>=1;i--){ rg int p=a[i]; siz[fa[p]]+=siz[p]; ans+=1LL*(len[p]-len[fa[p]])*siz[p]*(n-siz[p]); } printf("%lld\n",ans); } }sam; int main(){ sam.build(); sam.calc(); return 0; }