题意:
给出一个字符串s;
n次询问某个字符串xi的循环同构串在s中出现多少次;
|s|,∑|xi|<=10^6,n<=10^5;
题解:
WJMZBMR场的SAM题。。。
感觉还没学多久的后缀自动机姿势已经忘光了。。。悲伤哦;
首先考虑如何查询一个xi串在s中出现了多少次,这个只要直接用s的后缀自动机的trans指针匹配,然后得到的结点的right值就是答案了;
那么一个串xi的所有循环同构串就是将其倍长之后,里面长度为|xi|的子串们;
答案就是在后缀自动机上匹配,每次在后面加一个字符再在前面减一个字符,累加right值;
在前面减字符这个过程不太容易实现,所以转化一下,变成在需要的时候延pre指针向上走一次;
具体来讲这个正确的原因就是pre指针组成了反向后缀树,那么这个延反向后缀树向上相当于减去了一些前缀(减去长度未必为1,所以要在需要的时候减去);
注意累加答案的时候,经过了一次的结点不能再计算一遍,直接跳过即可,匹配的长度不够的结点也不应累加进去;
时间复杂度大概是线性的吧= =
代码:
#include<queue> #include<stdio.h> #include<string.h> #include<algorithm> #define N 2100000 #define S 26 using namespace std; char s[N],str[N]; namespace SAM { int son[N<<1][S],pre[N<<1],len[N<<1],right[N<<1],in[N<<1]; int vis[N<<1]; int last,tot; queue<int>q; int newnode() { return ++tot; } void init() { tot=0; last=newnode(); } void Insert(int x) { int np=newnode(),p; right[np]=1; len[np]=len[last]+1; for(p=last;p&&!son[p][x];p=pre[p]) son[p][x]=np; if(!p) pre[np]=1; else { int q=son[p][x]; if(len[q]==len[p]+1) pre[np]=q; else { int nq=newnode(); len[nq]=len[p]+1; pre[nq]=pre[q]; memcpy(son[nq],son[q],sizeof(int)*S); pre[q]=pre[np]=nq; for(;son[p][x]==q;p=pre[p]) son[p][x]=nq; } } last=np; } void Build() { int x,i; for(i=1;i<=tot;i++) in[pre[i]]++; for(i=1;i<=tot;i++) if(!in[i]) q.push(i); while(!q.empty()) { x=q.front(),q.pop(); right[pre[x]]+=right[x]; in[pre[x]]--; if(!in[pre[x]]) q.push(pre[x]); } } } int main() { int n,m,i,j,now,len,ans; scanf("%s",s+1); SAM::init(); m=strlen(s+1); for(i=1;i<=m;i++) SAM::Insert(s[i]-'a'); SAM::Build(); scanf("%d",&n); for(i=1;i<=n;i++) { scanf("%s",str+1); m=strlen(str+1); memcpy(str+m+1,str+1,sizeof(char)*m); for(j=1,ans=0,now=1,len=0;j<m+m;j++) { while(now&&SAM::son[now][str[j]-'a']==0) now=SAM::pre[now],len=min(len,SAM::len[now]); if(!now) now=1,len=0; else now=SAM::son[now][str[j]-'a'],len=min(len,SAM::len[now])+1; if(len>=m) { while(SAM::len[SAM::pre[now]]>=m) now=SAM::pre[now],len=min(len,SAM::len[now]); if(len>=m&&SAM::vis[now]!=i) ans+=SAM::right[now],SAM::vis[now]=i; } } printf("%d\n",ans); } return 0; }