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个

红包金额最低5元

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

抵扣说明:

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

余额充值