M - 图书馆
传送门
1. 大概题意:给定一个母串以及多个模式串,求有多少个模式串出现在母串中,注意:重复关键词重复统计。
2.思路分析:
这道题是多模匹配的问题,那么很明显可以用AC自动机来AC,这里我给出两种做法,一种用AC自动机,还有就是字典树来处理。下面看核心代码:
3.字典树代码:
定义结构体。注意这里的26是看情况而变化的,因为题目保证只有小写字母,所以26即可。
Createroot部分。每次 new 一个新的节点,并将所有信息都初始化。
插入操作。如果没到字符串结尾时,而且没有这个字符时,就新建一个,然后递推下去即可。
查找,删除(释放内存)。按每一个字符查找,如果没找到,即为null,那么就直接返回0,否则用 res 累计答案,循环下去。
释放内存,递归实现即可,不释放的话内存要炸。
主程序。怎么去查找,这是最开始一个比较困扰我的问题,因为字符串可能是在母串的中间进行了匹配,那么我就把模式串建树,查找就从母串的字符开始查找,看是否在字典树可以找到,问题就得到解决了。代码量还是比较小的。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define maxn 1000005
char s[maxn];
char tmp[54];
typedef struct Trie_node
{
struct Trie_node* next[26];
int cnt;
bool exist;
}TrieNode,*Trie;
int res;
Trie_node* Trie_createRoot()
{
Trie_node* root=new Trie_node();
root->cnt=0;
root->exist=false;
memset(root->next,0,sizeof(root->next));
return root;
}
void Trie_insert(Trie node,char *p)
{
while(*p){
if(node->next[*p-'a']==NULL){
node->next[*p-'a']=Trie_createRoot();
}
node=node->next[*p-'a'];
p++;
}
node->exist=true;
node->cnt++;
}
int TrieSearch(Trie node,char *p)
{
while(*p){
node=node->next[*p-'a'];
if(node==NULL)return 0;
if(node->exist)res+=node->cnt;
p++;
}
return 1;
}
void Triedelete(Trie root)
{
for(int i=0;i<26;i++){
if(root->next[i]!=NULL){
Triedelete(root->next[i]);
}
}
free(root);
}
int main(){
int t;
scanf("%d",&t);
int n;
while(t--){
res=0;
Trie root=Trie_createRoot();
scanf("%s",s);
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%s",tmp);
Trie_insert(root,tmp);
}
for(int i=0;s[i];i++){
TrieSearch(root,s+i);
}
cout<<res<<endl;
Triedelete(root);
}
}
4.AC自动机代码:
其实刚开始不知道AC自动机是干啥的,就自己看来别人的博客,发现是那么的神奇,orz.
简单说明一下AC自动机,首先是建立一个字典树,这里不再赘述;接着就是fail 指针的建立,在kmp 算法中,我们通过 next 数组来找到失配后下一个应该开始匹配的位置,当然 kmp 是单模式匹配,与 next 数组相似的就是 fail 指针,当发现某个字符失配时,就通过 fail 指针跳转,再次进行匹配,而这种多模式匹配就得益于fail 指针。
当前节点t有 fail 指针,那么 fail 指针所指的节点与 t 是相同的。因为 t 匹配失败后,直接跳到 fail 所指的继续匹配即可。
初始化。
插入操作。与字典树类似,注意我们最后是 tail[ p ]++,会重复记录,如果想去重的话, tail[ p ]=1就可以了。
构造失配指针。利用 bfs 。如果某个节点没有对应的字母,就跳转到 fail [ u ]所指的字母,否则就入队列。
查询操作。这里的 ans 是用来记录的,如果只是记录种类数,那么统计之后直接使得 tail[ p ]=0即可,不然就每次都计数。
//该程序不能判别相同模式串,因此若模式串重复,答案会将相同模式串当做不同的处理,因此若需要可以用map去重或修改insert
#include<stdio.h>
#include<string.h>
#include<queue>
using namespace std;
const int maxm=500006; //maxm是总结点数:约为字母数+++
char s[1000005],word[55];
char ss[1000005];
int nxt[maxm][26],tail[maxm],f[maxm],size; //nxt是结点指向不同字母的结点下标,tail是表示该结点为几个单词的词尾(可能需要计算重复的模式串情况),f是当不匹配时转跳到的结点下标,size是结点数
int newnode(){ //初始化整个trie或建立新的结点时,首先初始化当前结点所指向的26个字母的结点为0,表示暂时还没有指向的字母,然后暂定该结点不是单词尾结点,暂无失配时转跳位置(即转跳到根节点),返回结点标号
memset(nxt[size],0,sizeof(nxt[size]));
f[size]=tail[size]=0;
return size++;
}
void insert(char s[]){ //构造trie,p为当前结点的上一个结点标号,初始为0;x即为当前结点(上个结点标号指向当前字母的结点)标号,若此结点还未出现过,那么就建立这个结点;然后更新p为当前结点标号以便后续操作
int i,p=0;
for(i=0;s[i];i++){
int &x=nxt[p][s[i]-'a'];
p=x?x:x=newnode();
}
tail[p]++; //此时仅将s串记录,即将s串结尾的结点加1,若无相同模式串,则此操作只会使所有串尾结点的tail值由0变为1,但有相同模式串,则会重复记录,需要去重可以用map或用tail[p]=1;语句来完成
}
void makenxt(){ //利用bfs来构造失配指针
int i;
queue<int>q;
f[0]=0; //先将0结点挂的字母加入队列,失配指针指向0结点
for(i=0;i<26;i++){
int v=nxt[0][i];
if(v){
f[v]=0;
q.push(v);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(i=0;i<26;i++){
int v=nxt[u][i];
if(!v)nxt[u][i]=nxt[f[u]][i]; //当u结点没有i对应字母,则视为失配,将其指向失配后转跳到的结点所指向的i对应字母
else{
q.push(v); //u结点存在指向i的结点,则将所指向的结点下标加入队列
f[v]=nxt[f[u]][i]; //失配指针指向上个结点失配指针指向结点所挂当前字母的结点
}
}
}
}
int query(char s[]){ //查询s串中模式串出现了多少种/次
int ans=0,v=0;
for(int i=0;s[i];i++){
while(v&&!nxt[v][s[i]-'a'])v=f[v]; //先匹配直到没有失配
v=nxt[v][s[i]-'a'];
int tmp=v;
while(tmp){
ans+=tail[tmp];
// tail[tmp]=0; //这里加这句是为了仅计算出现多少种模式链,而若不加这句则可以计算累计出现多少次
tmp=f[tmp];
}
}
return ans;
}
int main(){
int T;
scanf("%d",&T);
while(T--){
size=0,newnode();
int n;
scanf("%s",s);
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%s",word);
insert(word);
}
makenxt();
printf("%d\n",query(s));
}
return 0;
}