AC自动机学习笔记


title: AC自动机学习笔记
date: 2020-08-31 15:53:40
tags:

  • 字符串
  • AC自动机
  • ACM

8月的最后一天,还是完成了ac自动机的学习。在熟练掌握了kmp后,我发现ac自动机并没有想象的那么难,既不难理解,也不难实现,于是决定写点东西记录一下。
本篇主要谈谈ac自动机的理论,思想。

写在开头

首先我们必须要明确一个问题,即ac自动机到底是干嘛的,我在查阅了许多文章与博客后发现他们并没有把这个问题阐释清楚,这里我解释一下。

我们知道,kmp算法可以在o(n)时间内进行对模式串的匹配工作,但kmp算法每次只能处理一个模式串,而ac自动机藉由字典树与fail指针(类似kmp中的next数组),可以在o(n)时间内完成对若干模式串的匹配工作,包括检查是否存在、出现了几次等等。ac自动机有一个常见的应用,即文本词频统计。

想学习掌握ac自动机,必须先掌握字典树,即trie树,并理解kmp的核心思想。

1.字典树

字典树又称单词查找树,Trie树,是一种树形结构。典型应用是用于统计,排序和保存大量的字符串(且不仅限于字符串)。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
字典树的实现形式多样,用起来也很灵活,后面会给一个比较简单的板子。
字典树可以单独使用,也可以包含进其他复杂数据结构中。
在ac自动机中,我们先将需要查询的单词全部插入到字典树中,再借此构造fail指针,然后进行模式匹配。

2.fail指针

2.1.作用

我们知道,kmp的核心思想就是利用已经匹配过的部分,减少重复匹配工作,避免回溯,fail指针的作用也与此类似。如果当前点匹配失败,则将指针转移到fail指针所指向的地方继续进行匹配。fail指针依靠的是什么?是当前模式串后缀和fail指针指向的模式串部分前缀相同。是不是很像next数组?next数组的定义就是最长公共前后缀,而这里我们可以理解为当前模式串与fail指针指向的模式串的公共前后缀,而这个公共前后缀,就是我们通过fail指针可以避免重复匹配的部分,并且是最长的。这样,我们只要从头到尾遍历一遍匹配字符串,就能完成所有模式串的匹配工作(这里要注意的一点是字典树里是没有相同单词的,或者说相同单词我们通过前面代码里的cnt数组保存下来了)。

2.2实现

本篇不讲求fail指针的过程,因为有讲的更好的,大家可以移步
https://bestsort.cn/2019/04/28/402/
这篇文章配有大量图解,理解起来很方便。

3.模式匹配

匹配过程也与kmp类似。我们从匹配串的开头开始,将每个字符与字典树中的字符匹配,需要注意的是:

  1. 在单次匹配中,如果当前字符与字典树中的字符匹配成功,那么将答案加上字典树中在这个字符结尾的单词的数量。
  2. 字典树中可能存在这样的单词,是当前模式串的后缀,因此我们还要通过fail指针找到它,将它出现的次数也统计上,直到fail指针指向根,或走到的节点已访问过。
  3. 在构建fail指针的过程中,如果当前节点不存在某个字母的儿子节点,我们会将其指向其fail指针所指向的位置。因此在单次匹配中若匹配失败,我们仍然可以顺着字典树继续匹配,实际上是完成了一次顺着fail指针转移的操作。

4.代码实现

4.1.字典树

int ch[maxn][26];//字典树
int cnt[maxn];//单词出现次数
int sz;
void init()
{
    sz = 1;
    memset(ch[0],0,sizeof(ch[0]));
    val[0] = 0;
    cnt[0] = 0;
}
void insert(char str[],int len)//插入字符串
{
    int u = 0;
    per(i,0,len)
    {
        int v = str[i]-'a';
        if(!ch[u][v])
        {
            memset(ch[sz],0,sizeof(ch[sz]));
            val[sz] = 0;    
            cnt[sz] = 0;
            ch[u][v] = sz++;
        }
        u = ch[u][v];
    }
    cnt[u]++;
    //在这里我们可以建立一个int-string的映射,以通过节点序号得知这个点是哪个单词的结尾
}
int findstr(char str[],int len)//查找字符串
{
    int u = 0;
    per(i,0,len)
    {
        int v = str[i]-'a';
        if(!ch[u][v])
            return 0;
        u = ch[u][v];
    }
    return cnt[u];
}

4.2.构建fail指针

int fail[maxn];
void getfail()
{
    //所有模式串已插入完成
    queue<int> q;
    per(i,0,26)
    {
        if(ch[0][i])
        {
            fail[ch[0][i]] = 0;
            q.push(ch[0][i]);
        }
    }
    while(!q.empty())
    {
        int now = q.front();
        q.pop();
        per(i,0,26)
        {
            if(ch[now][i])
            {
                fail[ch[now][i]] = ch[fail[now]][i];
                q.push(ch[now][i]);
            }
            else
                ch[now][i] = ch[fail[now]][i];
        }
    }
}

4.3.模式匹配

int query(char str[],int len)
{
    int now = 0,ans = 0;
    per(i,0,len)
    {
        now = ch[now][str[i]-'a'];
        int j = now;
        while(j && cnt[j]!=-1)
        {
            ans+=cnt[j];
            cnt[j] = -1;//防止重复计算。这里可以将j映射到string,以具体统计每个模式串出现的次数。j即节点序号,模式串与其是一一对应的关系
			j = fail[j];
        }
    }
    return ans;
}

9.11更新
在刷了一定量的题后,我对ac自动机有了新的理解,我发现ac自动机本质上其实就是在字典树上加边,从而构成了一张状态转换图,图上的每个节点都可以表示一个字符串。我们在进行模式匹配时,实质上就是在进行状态转换。藉由这个状态转换图,ac自动机可以与很多其他的算法知识结合起来,比如动态规划,最短路,矩阵快速幂等等。拿动态规划举例,我们在进行dp时常常会加一维,表示当前所在的状态转换图节点,而在使用ac自动机构成状态转换图时,我们可以在节点上附加信息,比如有时题目要求是不能存在某个串,那么我们可以在节点上标记一下,这个点不能到达;有时题目要求出现若干个串,在串的数量不大时我们可以利用状态压缩,用整形数据的每一个二进制位表示某个字符串是否出现过,这个可以放在dp数组的第三维,以表示当前匹配状态,既可以表示某个串是否出现过,也可以得到出现过几个不同的串。在进行dp时,状态转移方程就多了一维以表示当前所在的状态转换图节点,这样我们就省掉了繁杂的字符串匹配工作,直接借助图进行状态转换就行了。

我们知道构建fail指针实际上就是构建状态转换图的过程,而在构建fail指针时我们也可进行一定的修改。我们知道,fail指针指向的实际上是当前节点表示字符串的最长后缀,那么我们就可以在当前节点上标记一下,表示当匹配到这个节点时,fail指针指向的那个节点所代表的字符串也被匹配到了。而求fail指针时是用的bfs思想,因此我们可以逐层的进行处理。

有时我们要做的是这些串不出现,那么我们只要让标记数组进行逻辑或一下就可以了,如果当前节点所包含的后缀中有不能出现的字符串,那当前节点无疑也不能走。同样的,有时我们需要知道走到当前节点时,匹配到了哪些串。在一开始,我们是通过fail指针逐次的往根节点走,实际上我们可以利用上面说到的状态压缩思想,假设字符串编号为i,那标记就可以是1<<i ,这样我们通过一个整形就能得到若干个字符串是否可达的信息。当然这种方法只能处理字符串数量小于31的情况,借助长整型可以增加到63。

一般情况下这类问题的数据规模都不会很大,比如要让若干个串全部未出现过,那构造的目标串长度一般就10e4-10e5左右,dp就可以解决,但有时会遇到长度非常大的情况,我们就得考虑别的方法了。就拿这个问题举例,也就是poj2778,目标串长度+1时我们就可以理解为在状态转换图上走向了一个节点,这样问题就转化为在状态转换图中从根结点开始长度为n的,且不经过某些点的不同路径数量,这里的某些点即不能出现的串的终点所在节点。于是我们可以构造出状态转移矩阵,表示节点i和节点j之间有若干条边可走,接着就可以用矩阵快速幂加速转移过程了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值