UESTC Training for String AC自动机 or 字典树

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;
}

猜你喜欢

转载自blog.csdn.net/qq_40273481/article/details/80977705
今日推荐