AC自动机 (多模式匹配)

AC自动机

感谢博主:https://blog.csdn.net/bestsort/article/details/82947639

感谢博主 : https://fanfansann.blog.csdn.net/article/details/106160470

简介

KMP
Tire

要学AC自动机需要自备两个前置技能:KMP和trie树(其实个人感觉不会kmp也行,失配指针的概念并不难)

其中,KMP是用于一对一的字符串匹配,而trie虽然能用于多模式匹配,但是每次匹配失败都需要进行回溯,如果模式串很长的话会很浪费时间,所以AC自动机应运而生,如同Manacher一样,AC自动机利用某些操作阻止了模式串匹配阶段的回溯,将时间复杂度优化到了O ( n ) O(n)为文本串长度

建立AC自动机

建立一个AC自动机通常需要两个步骤:

  • 基础的TRIE结构:将所有的模式串构成一棵Trie。
  • KMP的思想:对Trie树上所有的结点构造失配指针。

然后就可以利用它进行多模式匹配了。

构造失配( fail )指针

在讲构造以前,先来对比一下这里的 fail 指针与KMP中的next指针:

共同点-两者同样是在失配的时候用于跳转的指针。
不同点-KMP要求的是最长相同真前后缀,而AC自动机只需要相同后缀即可。
因为KMP只对一个模式串做匹配,而AC自动机要对多个模式串做匹配。
有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同。
也就是说,AC自动机在对匹配串做逐位匹配时,同一位上可能匹配多个模式串。
因此 fail指针会在字典树上的结点来回穿梭,而不像KMP在线性结构上跳转。

下面介绍构建 fail指针的基础思想:(强调!基础思想!基础!)
构建 fail指针,可以参考KMP中构造next数组的思想。
我们利用部分已经求出 fail 指针的结点推导出当前结点的 fail 指针。具体我们用BFS实现:
考虑字典树中当前的节点u,u的父节点是p,p通过字符c的边指向u。
假设深度小于u的所有节点的 fail指针都已求得。那么p的 fail 指针显然也已求得。
我们跳转到p的 fail 指针指向的结点 fail[p] ;
如果结点 fail[p] 通过字母 c 连接到的子结点 w 存在:
则让u的fail指针指向这个结点 w( fail[u]=w )。
相当于在 p 和 fail[p] 后面加一个字符 c ,就构成了 fail[u] 。
如果 fail[p] 通过字母 c 连接到的子结点 w 不存在:
那么我们继续找到 fail[fail[p]] 指针指向的结点,重复上述判断过程,一直跳 fail 指针直到根节点。
如果真的没有,就令 fail[u]= 根节点。
如此即完成了 fail 指针的构建。

图解构造

  • 黄色结点表示当前的结点u,绿色结点表示已经BFS遍历完毕的结点,红/橙色的边表示 fail指针。
  • 2号节点的 fail 指针画错了, fail[2]=0

img

  • 我们重点分析结点6的 fail指针构建: AC_automation_6_9.png
  • 找到6的父节点5,5的 fail指针指向10,然而10结点没有字母’s’连出的边;
  • 所以跳到10的 fail 指针指向的结点0,发现0结点有字母’s’连出的边,指向7结点;
  • 所以 fail[6]=7

另外,在构建 fail指针的同时,我们也对TRIE中模式串的结尾构建 fail指针。这样在匹配到结尾后能自动跳转到下一个匹配项。具体见代码实现。

下面开始用图学习ac自动机吧(个人比较喜欢放图,能用一张图解决的绝不叨叨)
首先给定模式串"ash",“shex”,“bcd”,“sha”,然后我们根据模式串建立如下trie树:

img

然后我们再了解下一步:
ac自动机,就是在tire树的基础上,增加一个fail指针,如果当前点匹配失败,则将指针转移到fail指针指向的地方,这样就不用回溯,而可以路匹配下去了.(当前模式串后缀和fail指针指向的模式串部分前缀相同,如abce和bcd,我们找到c发现下一个要找的不是e,就跳到bcd中的c处,看看此处的下一个字符(d)是不是应该找的那一个)

一般,fail指针的构建都是用bfs实现的
首先每个模式串的首字母肯定是指向根节点的(一个字母你瞎指什么指,指了也是头字母有什么用嘛)

在这里插入图片描述

现在第一层bfs遍历完了,开始第二层
(根节点为第0层)第二层a的子节点为s,但是我们还是要从a-z遍历,如果不存在这个子节点我们就让他指向根节点(如下图红色的a)

在这里插入图片描述

当我们遍历到s的时候,由于存在s这个节点,我们就让他的fail指针指向他父亲节点(a)的fail指针指向的那个节点(根)的具有相同字母的子节点(第一层的s),也就是这样

在这里插入图片描述

按照相同规律构建第二层后,到了第三层的h点,还是按照上面的规则,我们找到h的父亲节点(s)fail指针指向的那个位置(第一层的s)然后指向它所指向的相同字母根->s->h的这个链的h节点,如下图

在这里插入图片描述

完全构造好后的树

在这里插入图片描述

然后匹配就很简单了,这里以ashe为例
我们先用ash匹配,到h了发现:诶这里ash是一个完整的模式串,好的ans++,然后找下一个e,可是ash后面没字母了啊,我们就跳到hfail指针指向的那个h继续找,还是没有?再跳,结果当前的h指向的是根节点,又从根节点找,然而还是没有找到e,程序END

过程如下图

模板


#include <queue>
#include <cstdlib>
#include <cmath>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn =  2*1e6+9;

int trie[maxn][26]; //字典树
int cntword[maxn];  //记录该单词出现次数
int fail[maxn];     //失败时的回溯指针
int cnt = 0;

void insertWords(string s){
    int root = 0;
    for(int i=0;i<s.size();i++){
        int next = s[i] - 'a';
        if(!trie[root][next])
            trie[root][next] = ++cnt;
        root = trie[root][next];
    }
    cntword[root]++;      //当前节点单词数+1
}
void getFail(){
    queue <int>q;
    for(int i=0;i<26;i++){      //将第二层所有出现了的字母扔进队列
        if(trie[0][i]){
            fail[trie[0][i]] = 0;
            q.push(trie[0][i]);
        }
    }

//fail[now]    ->当前节点now的失败指针指向的地方
tire[now][i] -> 下一个字母为i+'a'的节点的下标为tire[now][i]
    while(!q.empty()){
        int now = q.front();
        q.pop();

        for(int i=0;i<26;i++){      //查询26个字母
            if(trie[now][i]){
                //如果有这个子节点为字母i+'a',则
//让这个节点的失败指针指向(((他父亲节点)的失败指针所指向的那个节点)的下一个节点)
                //有点绕,为了方便理解特意加了括号

                fail[trie[now][i]] = trie[fail[now]][i];
                q.push(trie[now][i]);
            }
            else//否则就让当前节点的这个子节点
                //指向当前节点fail指针的这个子节点
                trie[now][i] = trie[fail[now]][i];
        }
    }
}


int query(string s){
    int now = 0,ans = 0;
    for(int i=0;i<s.size();i++){    //遍历文本串
        now = trie[now][s[i]-'a'];  //从s[i]点开始寻找
        for(int j=now;j && cntword[j]!=-1;j=fail[j]){
            //一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过).
            ans += cntword[j];
            cntword[j] = -1;    //将遍历国后的节点标记,防止重复计算
        }
    }
    return ans;
}

int main() {
    int n;
    string s;
    cin >> n;
    for(int i=0;i<n;i++){
        cin >> s ;
        insertWords(s);
    }
    fail[0] = 0;
    getFail();
    cin >> s ;
    cout << query(s) << endl;
    return 0;
}
  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AC自动机主要用于字符串匹配,多条件比较的话可以在AC自动机中添加多个模式串,并在匹配时同时匹配多个条件。例如,可以将多个关键字或多个正则表达式作为模式串,同时匹配多个条件。 具体实现可以参考以下步骤: 1. 将多个模式串添加到AC自动机中,每个模式串对应一个状态节点。 2. 在每个状态节点中,添加一个列表,用于存储与该节点匹配的所有模式串。 3. 在匹配时,对于每个输入字符,从当前状态节点开始,按照AC自动机的匹配规则进行匹配,同时将与匹配的模式串存储在状态节点中的列表中。 4. 匹配完成后,遍历所有匹配成功的状态节点,取出其列表中的模式串,并进行多条件比较。 例如,假设有三个关键字:"apple"、"banana"、"orange",需要同时匹配两个条件:长度大于等于5且以"a"开头。则AC自动机的构建和匹配流程如下: 构建AC自动机: 1. 将三个关键字添加到AC自动机中,得到以下状态图: 0 -a-> 1 -p-> 2 -p-> 3 -l-> 4 -e-> 5 ($"apple") 0 -b-> 6 -a-> 7 -n-> 8 -a-> 9 -n-> 10 -a-> 11 ($"banana") 0 -o-> 12 -r-> 13 -a-> 14 -n-> 15 -g-> 16 -e-> 17 ($"orange") 2. 在每个状态节点中添加一个列表,用于存储与该节点匹配的模式串。 0: [] 1: [] 2: [] 3: [] 4: [] 5: ["apple"] 6: [] 7: [] 8: [] 9: [] 10: [] 11: ["banana"] 12: [] 13: [] 14: [] 15: [] 16: [] 17: ["orange"] 匹配: 假设输入字符串为"an apple a day keeps the doctor away",则匹配流程如下: 1. 从状态0开始,输入字符"a",到达状态1。 2. 输入字符"n",到达状态8。 3. 输入字符" ",到达状态0。 4. 输入字符"a",到达状态1。 5. 输入字符"p",到达状态2,并将模式串"apple"存储在状态5的列表中。 6. 输入字符"p",到达状态3。 7. 输入字符"l",到达状态4。 8. 输入字符"e",到达状态5,并将模式串"apple"存储在状态5的列表中。 9. 输入字符" ",到达状态0。 10. 输入字符"a",到达状态1。 11. 输入字符"d",到达状态0。 12. 输入字符"a",到达状态1。 13. 输入字符"y",到达状态0。 14. 输入字符" ",到达状态0。 15. 输入字符"k",到达状态0。 16. 输入字符"e",到达状态0。 17. 输入字符"e",到达状态0。 18. 输入字符"p",到达状态0。 19. 输入字符"s",到达状态0。 20. 输入字符" ",到达状态0。 21. 输入字符"t",到达状态0。 22. 输入字符"h",到达状态0。 23. 输入字符"e",到达状态0。 24. 输入字符" ",到达状态0。 25. 输入字符"d",到达状态0。 26. 输入字符"o",到达状态0。 27. 输入字符"c",到达状态0。 28. 输入字符"t",到达状态0。 29. 输入字符"o",到达状态0。 30. 输入字符"r",到达状态0。 31. 输入字符" ",到达状态0。 32. 输入字符"a",到达状态1。 33. 输入字符"w",到达状态0。 匹配完成后,遍历状态5、11、17的列表,取出模式串"apple"和"orange",然后进行长度和开头字母的判断。符合条件的模式串为"apple",因为它的长度大于等于5且以"a"开头。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值