后缀自动机的应用
首先我们观察到:如果一个询问串的答案不为0,那么这个串一定是至少一个模式串的子串
如果只有一个模式串,那么这个问题可以简单地用什么东西解决掉(比如普通后缀自动机)
而这里有很多模式串,所以普通后缀自动机是不够的。
那么我们提出广义后缀自动机
所谓广义后缀自动机,可以简单理解成将很多个串建在同一个后缀自动机上
所以它的构造就是:每插入完毕一个串,就将las指针指回根节点,然后去构造下一个串就可以了
好像很简单?
上面的构造方法是不准确的!
这里转载一位dalao的博客,他详尽地给出了如上的构造方法的错误之处以及正确的方法
(但是太恶心了,而且一般错误的方法也不会被卡,所以我们这里还是使用了错误方法,但是在这里要有个印象,这并不是完全正确的构造方法)
接下来是转载部分:
"由于之前的疏忽,一直认为广义后缀自动机的构建方法就是在普通后缀自动机上直接插入多串,在此修正
原先的方法:对第一个串建后缀自动机,在插入第二个串时将fin指针指向根,然后暴力插入第二个字符串。
这种方法的错误:
当原来的字符串集合中含有这个字符串时,便会多建立新点,这个点并没有被任何点的tranc指向。
为什么之前一直没出问题:
我做题太少了
在大多数情况下,这个点是不会更新且不会被更新的,而且它前后的点都表现正常。
所以,在所有串都是静态的,就是一遍建成在大多数情况下是不会出现问题的。
那什么时候出现的问题:
当这个字符串集合是动态的(只插入不删除)时,一般我们使用LCT来维护Parent树。
这时,当我们拎出链进行修改时这个点就会参与运算,(然而这个点还没有记录原来修改的值)所以重复字符串/字符串前缀是需要在建立广义后缀自动机时进行特判处理
广义后缀自动机的本质:
普通后缀自动机是依靠字符串建立起来的。
而广义后缀自动机原则上是在原字符串集合的trie树上建立的,原则上要先建出trie树,然后记录每个点的fin指针。
每个节点在构建时需要在父亲的fin上构造。
原则上trie树的遍历可以使用dfs和bfs,但是dfs可以被构造的数据卡成O(n2)O(n2)
所以原则上要使用bfs来建广义后缀自动机。
但是考虑我们要解决的问题,也就是trie上构造和直接插入本质上的区别,就是trie树省略了前缀的重复
从这个性质入手,可以直接将后缀树的节点破开(特判一下就好了,就是前缀重复时像trie一样搞就好了)”
转载部分结束
接下来进入正题
我们还是按照老方法构造广义后缀自动机,本题不会被卡
然后我们分析一下题意:
如果一个询问串是一个模式串的子串,那么这个询问串一定是这个模式串的一个前缀的后缀(虽然我们常用的定义是后缀的前缀,但是在这个“后缀自动机”里我们使用第一个定义更容易理解)
那么,基于后缀自动机的pre指针的定义,我们发现:一个pre指针指向的点所对应的串一定是原节点对应字符串的一个后缀!
那么,如果我们将pre指针反指,就会得到一个树形结构,我们称这棵树叫parent树
我们可以发现,parent树的一个父节点对应的串是它所有子代节点(即儿子,儿子的儿子...)所对应串的子串!
以上内容,是在学习后缀自动机时应当了解到的,其实是基础知识,但是还是要介绍一下
那么对于这道题,我们发现:考虑到如果答案不为0,那么后缀自动机一定能识别这个串
所以我们可以先用建好的后缀自动机去识别每一个串,以此就可以查出所有答案为0的部分
然后,我们记录下答案不为0的询问串的结束位置,然后我们建起parent树求出dfs序,那么我们只需求出对于每个结束位置,它在parent树上的子树内有多少个不同的endpos即可(在广义后缀自动机上,用不同的endpos区分不同的串)
这一点可以利用dfs序实现
具体来讲,求出parent树的dfs之后,基于dfs序的性质,问题就转变成了在一段区间内(即询问节点的入栈序与出栈序)之间不同数值的个数
那么这类似于bzoj 1878,HH的项链
我们只需离线所有询问,然后用树状数组搞就可以了。
具体看代码
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
struct SAM
{
int tranc[27];
int endpos;
int pre;
int len;
}s[200005];
struct Edge
{
int next;
int to;
}edge[200005];
struct Ques
{
int lq,rq,num;
}q[60005];
int cnt=1;
int n,m;
char ch[360005];
int head[200005];
int inr[200005];
int our[200005];
int sum[400005];
int last[200005];
int ret[60005];
int f[400005];
int las,siz;
int dep;
int tot;
void init()
{
memset(head,-1,sizeof(head));
cnt=1;
}
bool cmp(Ques x,Ques y)
{
return x.rq<y.rq;
}
int lowbit(int x)
{
return x&(-x);
}
void add(int l,int r)
{
edge[cnt].next=head[l];
edge[cnt].to=r;
head[l]=cnt++;
}
void update(int x,int y)
{
while(x<=dep)
{
sum[x]+=y;
x+=lowbit(x);
}
}
int get_sum(int x)
{
int ans=0;
while(x)
{
ans+=sum[x];
x-=lowbit(x);
}
return ans;
}
void ins(int c,int typ)
{
int nwp=++siz;
s[nwp].endpos=typ;
s[nwp].len=s[las].len+1;
int lsp;
for(lsp=las;lsp&&!s[lsp].tranc[c];lsp=s[lsp].pre)s[lsp].tranc[c]=nwp;
if(!lsp)
{
s[nwp].pre=1;
}else
{
int lsq=s[lsp].tranc[c];
if(s[lsq].len==s[lsp].len+1)
{
s[nwp].pre=lsq;
}else
{
int nwq=++siz;
s[nwq]=s[lsq];
s[nwq].len=s[lsp].len+1;
s[nwq].endpos=0;
s[lsq].pre=s[nwp].pre=nwq;
while(s[lsp].tranc[c]==lsq)s[lsp].tranc[c]=nwq,lsp=s[lsp].pre;
}
}
las=nwp;
}
void buildtree()
{
init();
for(int i=2;i<=siz;i++)add(s[i].pre,i);
}
void dfs(int x)
{
inr[x]=++dep;
f[dep]=x;
for(int i=head[x];i!=-1;i=edge[i].next)
{
int to=edge[i].to;
dfs(to);
}
our[x]=++dep;
}
int check(int l)
{
int laas=1;
for(int i=1;i<=l;i++)
{
if(s[laas].tranc[ch[i]-'a'+1])laas=s[laas].tranc[ch[i]-'a'+1];
else return 0;
}
return laas;
}
int main()
{
scanf("%d%d",&n,&m);
las=++siz;
for(int i=1;i<=n;i++)
{
scanf("%s",ch+1);
int len=strlen(ch+1);
for(int j=1;j<=len;j++)ins(ch[j]-'a'+1,i);
las=1;
}
buildtree();
dfs(1);
for(int i=1;i<=m;i++)
{
scanf("%s",ch+1);
int len=strlen(ch+1);
int t=check(len);
if(t)
{
q[++tot].lq=inr[t];
q[tot].rq=our[t];
q[tot].num=i;
}
}
sort(q+1,q+tot+1,cmp);
int ttop=1;
for(int i=1;i<=dep;i++)
{
update(i,1);
if(last[s[f[i]].endpos])update(last[s[f[i]].endpos],-1);
last[s[f[i]].endpos]=i;
while(q[ttop].rq==i)
{
ret[q[ttop].num]=get_sum(q[ttop].rq)-get_sum(q[ttop].lq-1)-1;
ttop++;
}
}
for(int i=1;i<=m;i++)printf("%d
",ret[i]);
return 0;
}