资讯详情

简明扼要AC自动机

产生

全称Aho-Corasick automaton,该算法在1975贝尔实验室年产,是著名的多模匹配算法。

用途

  • 字符串的匹配
  • 多串匹配问题

例如,给几个单词 acbs,asf,dsef,

再给一篇很长的文章,acbsdfgeasf,

问这篇文章中出现了多少个单词,或者出现了多少个单词。

实现原理

KMP Trie树(字典树)

KMP,不懂的请出去右转https://blog.csdn.net/qq_38946354/article/details/97234737,是一种没有多余回溯的算法。

Trie字典树是一种用于快速检索的多叉树数据结构。用于保存大量字符串。它的优点是利用字符串的公共前缀来节省存储空间。实现方法可参考https://blog.csdn.net/qq_38946354/article/details/99902661

AC自动机的关键是在 Trie树上,加了 fail指针(失配指针)。fail 指针的用途就像 KMP中的 Next当字符串不匹配时,确定转移节点。

具体实现

构造字典树

struct Trie {     Trie *next[26];     Trie *fail;     int num;     Trie(){         for (int i = 0; i < 26; i  )             next[i] = NULL;         fail = NULL;         num = 0;     } }; char P[maxp]; char T[maxt]; Trie *q[maxq]; void insert(Trie *root, char *s) //字典树上 插入字符串s {     Trie *p = root;     for (int i = 0; s[i] != '\0'; i  ){         int c = s[i] - 'a';         if (p->next[c] == NULL){             p->next[c] = new Trie;         }         p = p->next[c];     }     p->num  ; }

构建失配指针

在构造完Tire树后,下一步就是构造失败指针。构造失败指针的过程总结为一句话:这个节点上的字母是 C,沿着父亲节点的失配指针走,直到它到达一个节点,它的子结点也有字母 C 的节点。然后将当前节点的失败指针指向字母 C 的儿子。假如一直到 root 如果找不到,指向失配指针 root。具体操作起来只需要:先把root加入队列(root失配指针指向自己或 NULL),在那之后,我们把它所有的儿子都加入了队列,每次处理一个点。

void build_ac_automation(Trie *root) //利用BFS创建AC自动机 {     int head = 0, tail = 0;  //队列首尾指针     q[tail  ] = root;     while (head != tail){         Trie *temp = q[head  ];         for (int i = 0; i < 26; i  ){  ///遍历队头元素的子结点             if (temp->next[i] != NULL){                 Trie *p = temp->fail;                 while (p != NULL){  //只有根结点的失配指针为 NULL                     if (p->next[i] != NULL){  //沿着失配指针回去,直到某个节点,                         temp->next[i]->fail = p->next[i];  //它有一个字母a' i的子结点                         break;                     }                     p = p->fail;  ///沿着失配指针一直在寻找                 }                 if (p == NULL)                     temp->next[i]->fail = root; //p==NULL 在沿着失配指针回去的过程中,没有找到合适的结点                 q[tail  ] = temp->next[i];  ///每处理一个结点,让结点的所有子节点依次入队             }         }     } }

为什么这种方法可行,可以保证 root 跳转位置的字符串长度小于当前匹配的字符串长度,与当前匹配的字符串后缀完全相同,长度最大?

显然,当我们构建失配指针时,我们从当前节点的父节点失配指针开始,因为 Trie 树压缩了所有单词中相同的前缀,所以所有不匹配的指针都不可能在同一水平上跳转(到达另一个与自己深度相同的节点),因为如果在同一水平上跳转,很明显,跳转到达的节点肯定不是当前匹配字符串后缀的一部分,否则两个节点将合并为一个,因此,跳转只能到达比当前深度小的节点,由于跳转是从当前节点的父节点开始的,因此可以保证从 root 跳转到位置的字符串长度小于当前匹配的字符串长度。另一方面,我们可以比较 KMP 求 NEXT 在数组中寻求最大匹配数量的想法,这种想法是 AC 自动机的体现是,当构建失败指针时,不断返回以前的跳转位置,然后判断跳转位置的下一个字符是否包含当前字符。如果是,将失败指针连接到跳转位置,若跳转位置指向 NULL 这意味着当前匹配的字符在当前深度之前没有出现,不能匹配任何跳转位置,如果发现第一个跳转位置的下一个字符包含当前字符的跳转位置,必须达到最大长度,这是因为当前匹配的其他字符必须高于当前字符的跳转位置深度,即使可以,这样的跳转位置也不会是最大的(最后一个字符的深度小于最后一个字符的深度,最后一个字符的最后一个字符在第一个可行的跳转位置,串必须更短)。

匹配

匹配过程分为两种情况:(1)当前字符匹配,表示从当前节点沿树边有一条路径到达目标字符。此时,只需沿路径到下一个节点继续匹配,目标字符串指针移动到下一个字符继续匹配;(2)如果当前字符不匹配,则继续匹配当前节点失败指针指向的字符。匹配过程随指针指向root结束。重复这两个过程中的任何一个,直到模式串结束。

int query(Trie *root, char *T) {     int ans = 0;     Trie *p = root;     for (int i = 0, len = strlen(T); i < len; i  ){         int c = T[i] - 'a';         while (p->next[c] == NULL && p != root) //如果当前结点没有字符 T[i] 儿子目前不是根节点             p = p->fail;                        ///沿着失配指针回去,直到找到合适的节点或根结点         if (p->next[c] != NULL)             p = p->next[c];         Trie *temp = p;         while (temp != NULL)             p = p->next[c];         Trie *temp = p;         while (temp != root && temp->num != -1){ ///沿着失配指针回去,直到根结点             ans  = temp->num; //如果当前节点num不为 0,说明以当前节点字母结尾的单词出现了                               ///这个单词是以上循环的结点字母为结尾的单词子集             temp->num = -1;   //标记 num 为-1,避免重复计算             temp = temp->fail;         }     }     return ans; }

完整代码

#include <cstdio>
#include <cstring>
using namespace std;
const int maxp = 50 + 7;
const int maxt = 1000000 + 7;
const int maxq = 10000 * maxp;
struct Trie
{
    Trie *next[26];
    Trie *fail;
    int num;
    Trie(){
        for (int i = 0; i < 26; i++)
            next[i] = NULL;
        fail = NULL;
        num = 0;
    }
};
char P[maxp];
char T[maxt];
Trie *q[maxq];
void insert(Trie *root, char *s) //在字典树上 插入字符串s
{
    Trie *p = root;
    for (int i = 0; s[i] != '\0'; i++){
        int c = s[i] - 'a';
        if (p->next[c] == NULL){
            p->next[c] = new Trie;
        }
        p = p->next[c];
    }
    p->num++;
}
void build_ac_automation(Trie *root) //利用BFS创建AC自动机
{
    int head = 0, tail = 0;  //队列首尾指针
    q[tail++] = root;
    while (head != tail){
        Trie *temp = q[head++];
        for (int i = 0; i < 26; i++){  //遍历队头元素的子结点
            if (temp->next[i] != NULL){
                Trie *p = temp->fail;
                while (p != NULL){  //只有根结点的失配指针为 NULL
                    if (p->next[i] != NULL){  //顺着失配指针往回走,直至某个节点,
                        temp->next[i]->fail = p->next[i];  //其拥有一个字母为'a'+i的子结点
                        break;
                    }
                    p = p->fail;  //沿着失配指针一直找
                }
                if (p == NULL)
                    temp->next[i]->fail = root; //p==NULL 说明顺着失配指针往回走的过程中没有找到合适的结点
                q[tail++] = temp->next[i];  //每处理一个结点,都让该结点的所有子节点依次入队
            }
        }
    }
}
int query(Trie *root, char *T)
{
    int ans = 0;
    Trie *p = root;
    for (int i = 0, len = strlen(T); i < len; i++){
        int c = T[i] - 'a';
        while (p->next[c] == NULL && p != root) //若当前结点的没有一个字符为 T[i] 的儿子且当前不是根节点
            p = p->fail;                        //顺着失配指针往回走,直至找到合适的节点或根结点为止
        if (p->next[c] != NULL)
            p = p->next[c];
        Trie *temp = p;
        while (temp != root && temp->num != -1){ //顺着失配指针往回走,一直到根结点
            ans += temp->num; //若当前节点的num不为 0,则说明以当前节点字母结尾的单词出现过
                              //此单词是以上一次循环的结点字母为结尾的单词的子集
            temp->num = -1;   //标记 num 为-1,避免重复计算
            temp = temp->fail;
        }
    }
    return ans;
}
int main()
{
    int t, n;
    scanf("%d", &t);
    while (t--){
        Trie *root = new Trie;
        scanf("%d", &n);
        getchar();
        for (int i = 0; i < n; i++){
            gets(P);
            insert(root, P);
        }
        build_ac_automation(root);
        gets(T);
        printf("%d\n", query(root, T));
    }
    return 0;
}

 

部分参考来自https://blog.csdn.net/liu940204/article/details/51347064

 

标签: 12dsef功率继电器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司