题目链接:https://ac.nowcoder.com/acm/problem/20443
思路:这道题给出了很多的单词,现在问每一个单词在所有单词中出现的总次数。步骤就是先建一棵字典树,再构建fail指针,最后把整棵字典树的节点遍历一遍。
我们可以在建字典树的时候可以先把每个节点经过的次数先算出来,因为字典树上的每一个节点都代表一个不同的字符串(从根节点到这个节点的路径唯一),所以我们字典树上每一个节点一开始计算出来的经过的次数就是这个节点所代表的字符串作为所有输入单词前缀的次数。
例如我们有单词aa,aab,aba,那么假设字典树上从根节点到某一个节点u构成的字符串是aa,那么我们在建字典树时,经过点u的次数就是2,因为aa在这些单词中作为前缀出现的次数就是2(aa,aab,aba)。
然后我们再构建fail指针,fail[u]其实就代表从根节点到fail[u]构成的这个前缀和以节点u结尾的长度相同的后缀是一样的,而我们已经把字符串作为前缀出现的次数算出来了,我们只要将每一个前缀在整棵树上作为其他串后缀的次数计算出来,那么这个字符串出现的总次数也就是两个次数相加。而这个总次数我们可以依靠fail指针来线性求出,在我们构建fail指针的时候,我们肯定是先遍历到每一个前缀,所以这个前缀肯定会先出现在队列中,在队列中的位置会排在其他字符串-->(这些字符串的后缀中,有一个是我们所说的这个前缀)前面。所以我们可以逆序遍历一遍所有队列中的节点,把每一个点统计的次数向fail指针的方向叠加。最终我们就可以得到字典树上所有前缀在输入单词中出现的次数了。
代码:
#include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<map> #include<stack> #include<cmath> #include<vector> #include<set> #include<cstdio> #include<string> #include<deque> using namespace std; typedef long long ll; #define eps 1e-8 #define INF 0x3f3f3f3f #define maxn 2000005 int q[maxn]; int tot=0,cnt=0,n; int fail[maxn],trie[maxn][27],num[maxn],pos[maxn];//num数组记录次数,pos数组记录每一个单词的结尾在字典树上的节点编号 char s[maxn]; void insert(int len,int index){ int root=0; for(int i=0;i<len;i++){ int c=s[i]-'a'; if(trie[root][c]==0) trie[root][c]=++cnt; root=trie[root][c]; num[root]++;//num[i]记录以节点i结尾的字符串(前缀)出现的次数 } pos[index]=root;//记录一下这个单词结尾的节点编号 } void build_fail(){//构建fail指针 for(int i=0;i<26;i++){ if(trie[0][i]){ fail[trie[0][i]]=0; q[tot++]=trie[0][i]; } } int now=0; while(now<tot){ int u=q[now++]; for(int i=0;i<26;i++){ if(trie[u][i]){ fail[trie[u][i]]=trie[fail[u]][i]; q[tot++]=trie[u][i]; }else{ trie[u][i]=trie[fail[u]][i]; } } } } int main() { scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%s",s); int len=strlen(s); insert(len,i); } build_fail(); for(int i=tot-1;i>=0;i--) num[fail[q[i]]]+=num[q[i]];//向fail指向的方向将次数叠加 for(int i=0;i<n;i++) printf("%d\n",num[pos[i]]);//之前记录过了每一个单词结尾的位置,这里直接输出 return 0; }