zoukankan      html  css  js  c++  java
  • AC自动机

    多重匹配求连续子串问题
    AC自动机入门题
    对于字符串匹配可以用kmp,但是对于多个字符串匹配呢
    用ac自动机
    也就是kmp+trie

    大佬博客

    ac自动机

    模式串he,she,him,hers,shit构成的trie树

    然后去查询fail指针

    fail指针的理解:

    是把下层的去指向上层
    而对于上层的子串必定是下层的子串,所有说如果能匹配下层,必定能匹配上层(利用这个减少时间复杂度)
    
    第一层(根下面的一层)肯定是指向根的
    非第一层的,如果满足不了上述则也指向根,表示该子串和前面的子串没有公共前缀,要重新开始匹配
    

    模板

    传送门

    • 对子字符串进行构建trie树
    • 求失配指针
    • 用主串进行匹配
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    using namespace std;
    const int maxn = 1e6 + 5;
    int cnt = 0;//trie的指针
    struct tree{
        int fail;//失配指针
        int vis[26];//子结点的位置
        int end;//标记有几个单词以这个节点结尾
    }ac[maxn];//trie树
    void bulid(char *t){//对字典树进行初始化
        int len = strlen(t);
        int now = 0;//字典树当前的指针
        for(int i = 0; i < len; i++){
            if(ac[now].vis[t[i]-'a'] == 0){//trie树里没有这个子结点
                ac[now].vis[t[i]-'a'] = ++cnt;
            }
            now = ac[now].vis[t[i]-'a'];//向下构造
        }
        ac[now].end += 1;
    }
    void get_fail(){//构建fail指针
        queue<int>q;
        for(int i = 0; i < 26; i++){//对第一层的进行处理,全部指向根(第0层)
            if(ac[0].vis[i] != 0){
                ac[ac[0].vis[i]].fail = 0;
                q.push(ac[0].vis[i]);//同时把第一层的所有子结点压入队列里
            }
        }
        while(!q.empty()){
            int u = q.front();
            q.pop();
            for(int i = 0; i < 26; i++){
                if(ac[u].vis[i] != 0){//存在这个子结点
                    ac[ac[u].vis[i]].fail = ac[ac[u].fail].vis[i];
                    q.push(ac[u].vis[i]);
                }else{//不存在子结点
                    ac[u].vis[i] = ac[ac[u].fail].vis[i];
                }
            }
        }
    }
    int ac_query(char *s){
        int len = strlen(s);
        int now = 0, ans = 0;
        for(int i = 0; i < len; i++){
            now = ac[now].vis[s[i] - 'a'];
            for(int t = now; t && ac[t].end != -1; t = ac[t].fail){
                ans += ac[t].end;//只有单词都匹配到,且匹配到结尾才会加非零数
                ac[t].end = -1;
            }
        }
        return ans;
    }
    int main(){
        int n;
        scanf("%d", &n);
        char s[maxn];//主串
        char t[maxn];//子串
        for(int i = 0; i < n; i++){
            scanf("%s", t);//子串
            bulid(t);
        }
        ac[0].fail = 0;//结束标记
        get_fail();
        scanf("%s", s);//主串
        printf("%d
    ", ac_query(s));
    
        return 0;
    }
    

    优化版模板

    • 根的失配是根
    • 先把所有根没指向的字母的失配指针指向根---初始化
    • 根指向的字母的的下一个字母失配指向根---初始化,且把下一个字母放入队列
    • 循环操作,寻找失配指针
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    using namespace std;
    const int maxn=1e6+5;
    struct ac_auto{
        int fail[maxn];//失配指针
        int vis[maxn][26];//子结点的位置
        int end[maxn];//标记有几个单词以这个节点结尾
        int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
    
        int newNode(){
            for(int i=0;i<26;i++){//26叉树
                vis[L][i]=-1;
            }
            end[L++]=0;
            return L-1;//返回节点编号
        }
        void initial(){
            L=0;
            root=newNode();//把第一行清空了,root=0,L=1
        }
        void Insert(char *t){
            int len=strlen(t);
            int now=root;
            for(int i=0;i<len;i++){
                int x=t[i]-'a';
                if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
                now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
            }
            end[now]++;
        }
        void get_fail(){
            queue<int>q;
            fail[root]=root;//根指向根
            for(int i=0;i<26;i++){//对于根结点下的第一排字母(单词的首个字母)
                if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
                else{//存在这个字母
                    fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
                    q.push(vis[root][i]);//再把这个字母的下一个字母放进去
                }
            }
    
            while(!q.empty()){
                int now=q.front();
                q.pop();
                for(int i=0;i<26;i++){
                    if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
                    else{
                        fail[vis[now][i]]=vis[fail[now]][i];
                        q.push(vis[now][i]);
                    }
                }
            }
        }
    
        int Query(char *s){
            int now=root;
            int ans=0;
            int len=strlen(s);
    
            for(int i=0;i<len;i++){
                now=vis[now][s[i]-'a'];
                int temp=now;
                while(temp!=root){
                    ans+=end[temp];
                    end[temp]=0;
                    temp=fail[temp];
                }
            }
            return ans;
        }
    }ac;
    int t,n;
    char s[maxn];
    char str[maxn];
    int main(){
        scanf("%d",&t);
        while(t--){
            scanf("%d",&n);
            ac.initial();
            for(int i=0;i<n;i++){
                scanf("%s",s);
                ac.Insert(s);
            }
            ac.get_fail();
            scanf("%s",s);
            printf("%d
    ",ac.Query(s));
        }
    }
    

    题目

    记录哪几个单词出现过

    传送门

    /*
    和模板比,就加了个used来记录这个单词是否被记录过
    且把end改成了记录结点而不是记录几次出现,(对于每一个字典树的点,只记录了唯一的一个数值)
    同时把26个字母改成了128个
    */
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    using namespace std;
    const int maxn=1e6+5;
    struct ac_auto{
        int fail[maxn];//失配指针
        int vis[maxn][128];//子结点的位置
        int end[maxn];//标记有几个单词以这个节点结尾
        int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
        bool used[510];
    
        int newNode(){
            for(int i=0;i<128;i++){//26叉树
                vis[L][i]=-1;
            }
            end[L++]=0;
            return L-1;//返回节点编号
        }
        void initial(){
            L=0;
            root=newNode();//把第一行清空了,root=0,L=1
        }
        void Insert(char *t,int id){
            int len=strlen(t);
            int now=root;
            for(int i=0;i<len;i++){
                int x=t[i];//直接赋值为ASCII码
                if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
                now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
            }
            end[now]=id;//now的值是唯一的
    
        }
        void get_fail(){
            queue<int>q;
            fail[root]=root;//根指向根
            for(int i=0;i<128;i++){//对于根结点下的第一排字母(单词的首个字母)
                if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
                else{//存在这个字母
                    fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
                    q.push(vis[root][i]);//再把这个字母的下一个字母放进去
                }
            }
    
            while(!q.empty()){
                int now=q.front();
                q.pop();
                for(int i=0;i<128;i++){
                    if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
                    else{
                        fail[vis[now][i]]=vis[fail[now]][i];
                        q.push(vis[now][i]);
                    }
                }
            }
        }
    
        int Queue(char *s){
            memset(used,false,sizeof(used));
            int now=root;
            int ans=0;
            int len=strlen(s);
    
            for(int i=0;i<len;i++){
                int x = s[i];
                now=vis[now][x];
                int temp=now;
                while(temp!=root){
                    if(end[temp]!=0){
                        used[end[temp]]=true;
                        ans =1;
                    }
                    temp=fail[temp];
                }
            }
            return ans;
        }
    }ac;
    int t,n;
    char s[maxn];
    char str[maxn];
    int main(){
        int m;
        scanf("%d",&m);
        ac.initial();
        for(int i=1;i<=m;i++){
            scanf("%s",str);
            ac.Insert(str,i);
        }
        ac.get_fail();
        int n;
        cin>>n;
        int ans=0;
        for(int i=1;i<=n;i++){
            scanf("%s",s);
            if(ac.Queue(s)!=0){
                printf("web %d:",i);
                for(int i=1;i<=m;i++){
                    if(ac.used[i]!=false){
                        printf(" %d",i);
                    }
                }
                ans++;
                putchar('
    ');
            }
        }
        printf("total: %d
    ",ans);
    }
    

    每个单词出现的次数,可重叠

    /*
    和模板比
    end改成了记录结点而不是记录几次出现,(对于每一个字典树的点,只记录了唯一的一个数值)
    同时把26个字母改成了128个
    而且把单词编号加入了Hash来寻找哪个单词,并且记录出现的次数
    还有一个记录字符串的结构体
    */
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    using namespace std;
    const int maxn=2e6+5;
    int Hash[1005];
    struct node{
        char ss[55];
    }words[1005];
    struct ac_auto{
        int fail[maxn];//失配指针
        int vis[maxn][128];//子结点的位置
        int end[maxn];//标记有几个单词以这个节点结尾
        int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
    
        int newNode(){
            for(int i=0;i<128;i++){//26叉树
                vis[L][i]=-1;
            }
            end[L++]=0;
            return L-1;//返回节点编号
        }
        void initial(){
            L=0;
            root=newNode();//把第一行清空了,root=0,L=1
        }
        void Insert(char *t,int id){
            int len=strlen(t);
            int now=root;
            for(int i=0;i<len;i++){
                int x=t[i];//直接赋值为ASCII码
                if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
                now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
            }
            end[now]=id;
    
        }
        void get_fail(){
            queue<int>q;
            fail[root]=root;//根指向根
            for(int i=0;i<128;i++){//对于根结点下的第一排字母(单词的首个字母)
                if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
                else{//存在这个字母
                    fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
                    q.push(vis[root][i]);//再把这个字母的下一个字母放进去
                }
            }
    
            while(!q.empty()){
                int now=q.front();
                q.pop();
                for(int i=0;i<128;i++){
                    if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
                    else{
                        fail[vis[now][i]]=vis[fail[now]][i];
                        q.push(vis[now][i]);
                    }
                }
            }
        }
    
        void Query(char *s){
            int now=root;
            int ans=0;
            int len=strlen(s);
    
            for(int i=0;i<len;i++){
                int x = s[i];
                now=vis[now][x];
                int temp=now;
                while(temp!=root){
                    if(end[temp]!=0){
                        Hash[end[temp]]++;
                    }
                    temp=fail[temp];
                }
            }
        }
    }ac;
    int t,n;
    char s[maxn];
    int main(){
        int m;
        while(cin>>m){
            ac.initial();
            memset(Hash,0,sizeof(Hash));
            for(int i=1;i<=m;i++){
                scanf("%s",words[i].ss);
                ac.Insert(words[i].ss,i);
            }
            ac.get_fail();
            scanf("%s",s);
            ac.Query(s);
            for(int i=1;i<=m;i++){
                if(Hash[i]!=0){
                    printf("%s: %d
    ",words[i].ss,Hash[i]);
                }
            }
        }
    }
    
    
  • 相关阅读:
    POJ 1436 Horizontally Visible Segments(线段树)
    POJ 1436 Horizontally Visible Segments(线段树)
    精益项目管理的可行性分析
    精益项目管理的可行性分析
    精益项目管理的可行性分析
    精益项目管理的可行性分析
    单点登录cas常见问题(二)
    单点登录cas常见问题(二)
    蓝氏兄弟依靠板栗东山再起,意外赚回八九万元
    元旦快乐,感谢一路相伴!
  • 原文地址:https://www.cnblogs.com/Emcikem/p/11544152.html
Copyright © 2011-2022 走看看