题目描述
小张最近在忙毕设,所以一直在读论文。一篇论文是由许多单词组成但小张发现一个单词会在论文中出现很多次,他想知道每个单词分别在论文中出现了多少次。
输入输出格式
输入格式:
第一行一个整数N,表示有N个单词。接下来N行每行一个单词,每个单词都由小写字母(a-z)组成。(N≤200)
输出格式:
输出N个整数,第i行的数表示第i个单词在文章中出现了多少次。
分析
这题做了半天,但AC时还是很高兴.....
拿到这个题首先想的是用Hash,空间也够开。但是总有丧心病狂的出题人,所以需要用好一点的办法,比如我只会的AC自动机。
AC自动机在构造过程中,会给每个字符串结束的节点赋值,可以用来统计完整的字符串。但这题每个字符串可以作为其它串的子串。
不妨在构造时每个到达的节点权值都+1,表示这个字符串的前缀出现一次。
接着理解next(或者fail数组)的意义:next[i]表示到了i节点,无法继续往下匹配时,满足最长的相关前后缀关系(这个关系就是KMP算法里的)的其它节点,也可以说是转移节点,使得可以沿着这个节点,像KMP算法一样继续往下匹配。
所以可以得到的信息是:类似于KMP算法,如果i号节点及其以前某些边构成了一个字符串,那么从根节点到next[i]也构成了这样的字符串,即:i号节点的计数,要加到到next[i]上。因为next[i]的深度不会超过i,所以按照深度从下往上统计是一个好方法。
如何做到从下往上统计?回顾构造AC自动机next数组的过程,正是因为next[i]的深度不会大于i,所以采用BFS,而BFS正是按照深度优先入队!!!这意味着next数组造好了,再把队列里的元素倒着取一遍就得解了。
那么只需要在插入每个字符串时,保存一个它结束的节点编号即可。为什么正确?因为这个编号可以保证从根节点到它,是整个树上构成这个串最短的一条路,所有的答案从下统计上来都会在这里截止,没有next可以转向更短的路径。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int MAXN=1000010;
int N,np=1;
int ch[MAXN][26];
int next[MAXN];
int p[210];
int val[MAXN];
char s[MAXN];
void insert(char *s,int id)
{
int now=1;
int len=strlen(s);
for(int i=0;i<len;i++)
{
if(!ch[now][s[i]-'a']) ch[now][s[i]-'a']=++np;
++val[now=ch[now][s[i]-'a']];
}
p[id]=now;
}
int q[MAXN],front=0,rear=0;
void BFS()
{
for(int i=0;i<26;i++) ch[0][i]=1;
next[0]=1;q[rear++]=1;
while(front!=rear)
{
int now=q[front++];
int back=next[now];
for(int d=0;d<26;d++)
{
if(!ch[now][d]) ch[now][d]=ch[back][d];
else
{
q[rear++]=ch[now][d];
next[ch[now][d]]=ch[back][d];
}
}
}
}
void ques()
{
for(int i=rear-1;i>=0;i--) val[next[q[i]]]+=val[q[i]];
for(int i=1;i<=N;i++) printf("%d\n",val[p[i]]);
}
int main()
{
scanf("%d",&N);
for(int i=1;i<=N;i++)
{
scanf("%s",s);
insert(s,i);
}
BFS();ques();
return 0;
}