[TJOI2013]单词 AC自动机
题面:
题解:
很久之前做的题了,只不过之前一直90.。。。最近才发现是哪里写错了。
我们对字符集建AC自动机。
首先考虑一个暴力的做法,把文章当做一个长串,直接在自动机上跳,但是我们会发现,这样的复杂度可能退化到$n^2$.
因为对于一个类似于aaaaaaaaaaaaaaaa这样的串而言,一个点的fail总是指向它的父亲,因此如果我们每次都暴力向上跳fail复杂度就不对了。
观察到每遍历到一个节点,其实质就是给这个点到root的这条链上的每个点都+1,因此我们目标只是在fail树上对每个点都求出子树和。
如果我们知道了所有标记的位置,显然可以建树统计一下。
当然,也有更方便的写法,因为一个点的编号肯定比它的fail的编号大。表现在fail树上就是父亲编号比儿子小。
所以我们从大到小枚举编号,把当前枚举到的节点的权值加给父亲即可。
为什么这样是对的?
因为我们可以看做是儿子先把代价给父亲,在让父亲把它的代价和它儿子的代价一起向上传。
其实对于这道题而言,还有更方便的写法,你甚至不需要建匹配串。因为文章就是由给定字符集组成的,因此我们一定会遍历自动机上的每一个节点,所以与其再遍历一遍,再把每个节点权值+1,不如在建自动机的时候就直接加。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define R register int 4 #define maxn 2500500 5 int n,ans[maxn],go[maxn],num;//num标记是第一个单词,便于处理单词重复的情况 6 int q[maxn],head,tail,tot;//从第一位开始存文本串 7 char s[maxn]; 8 9 struct ACtree 10 { 11 int fail[maxn], c[maxn][27]; 12 //由于是统计单词在文章中的出现次数,相同单词只算一次,所以val最大只能为1 13 void add()//标记是第一个单词 14 { 15 int now=0,len=strlen(s); 16 for(R i=0;i<len;i++) 17 { 18 int v=s[i]-'a'; 19 if(!c[now][v]) c[now][v]=++tot; 20 now=c[now][v], ++ ans[now];//因为之后再遍历也是直接遍历整个树,所以直接在这里加 21 } 22 // if(!val[now])val[now]++;//这样val就没用了 23 go[num]=now;//记录下每个单词的结尾部分,以防遇到重复只输出一个 24 } 25 26 void build() 27 { 28 R now; 29 for(R i=0; i<26 ;i++) 30 if(c[0][i]) q[++tail]=c[0][i];//既然fail一开始都为0,那就不用额外初始化了 31 while(head<tail) 32 { 33 now=q[++head]; 34 for(R i=0;i<26;i++) 35 if(c[now][i]) fail[c[now][i]]=c[fail[now]][i],q[++tail]=c[now][i]; 36 else c[now][i]=c[fail[now]][i];//建立虚拟节点 37 } 38 for(R i = tail; i; i --) ans[fail[q[i]]] += ans[q[i]]; 39 for(R i = 1; i <= n; i ++) printf("%d\n", ans[go[i]]); 40 } 41 42 }AC; 43 44 void pre() 45 { 46 scanf("%d",&n); 47 for(num=1; num<=n ;num++) 48 scanf("%s",s), AC.add(); 49 } 50 51 int main() 52 { 53 // freopen("in.in","r",stdin); 54 pre(); 55 AC.build(); 56 // fclose(stdin); 57 return 0; 58 }