AC自动机、后缀数组(SA)学习小记(填坑ing)

AC自动机

Aho-Corasick automaton?自动AC机!

第一次看见“AC自动机”以为是用来黑掉OJ的自动AC的神妙机器……尴尬……

咳咳AC自动机,全称Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法
上面那话没个屁用

  • 单模匹配就是给你一个单词、一个字符串,问你这个单词是否在这个字符串中出现过

  • 当然用KMP可以很好很快速地解决这个问题

多模匹配是什么?

  • 给出很多个单词、一个字符串,问你有多少个单词在这个字符串中出现过

  • 明显多个KMP暴力做GG,这时就要用到AC自动机

不是Tire2.0

AC自动机是一个非常玄妙、神奇、高深莫测的算法,但却不难
建立在Tire的基础上,而核心思想就是多了一个fail失配指针来实现多模匹配

其实和LCT splay + + 树剖差不多,AC自动机 KMP + + Tire
学这种东西没图还是不行啊……

无图无真相

请务必仔仔细细阅读以下内容否则AC自动机你就学不会了

  • fail f a i l 指针指向的到底是啥?

  • 每个节点的fail失配指针指向的是以当前节点表示的字符串为最后一个字符的最长当前字符串的后缀字符串的最后一个节点

绕口令我才懒得背……

还是看图容易理解本人鼠绘辣鸡

《AC自动机、后缀数组(SA)学习小记(填坑ing)》

此Tire中共有四个单词:ABCD、ABD、CD、BCE

  • 根节点到 x x 节点表示的字符串为ABC,而在Tire中它的存在最长后缀是BC

  • 所以 x x 节点的 fail f a i l 指向BC最后一个字符表示的节点—— y y

  • 至于 y y 点的 fail f a i l 指向 z z 点也就不难理解啦

  • 在Tire中存在的BC的最长后缀为C,于是 y y fail f a i l 就指向 z z

好像很好理解?因为本来AC自动机就不太难
关键是怎么建Tire和fail

建Tire和fail

insert

insert i n s e r t 就是向 Tire T i r e 里插入一个字符串,很基础的操作
网上的代码怎么都打的是指针什么的鬼畜得很
可以用二维数组代替了,好打得多

code

int next[MAXN][26],fail[MAXN],sum[MAXN];
queue<int>que;
int root,tot;

int newnode()
{
    fo(i,0,25)next[tot][i]=-1;
    sum[tot]=0;
    return tot++;
}

void init()
{
    tot=0;
    root=newnode();
}

void insert(char s[])
{
    int len=strlen(s),now=root;
    for (int i=0;i<len;i++)
    {
        if (next[now][s[i]-'a']==-1)next[now][s[i]-'a']=newnode();
        now=next[now][s[i]-'a'];
    }
    sum[now]++;
}

build_fail

其实建 fail f a i l 非常好建,只不过脑洞有点大

  • 首先, root r o o t 以及 root r o o t 的子节点 fail f a i l 都是指向 root r o o t

  • 对于其他节点, x x 节点代表的字符为 y y ,那么 x x fail f a i l 就为 fa[x]fail f a [ x ] f a i l 的儿子节点中代表字符为 y y 的节点

再连上一条 fail f a i l 来观察
《AC自动机、后缀数组(SA)学习小记(填坑ing)》

可以清楚看到, x x 点的父亲的 fail f a i l 指向的就是 y y 的父亲
因为 y y 点父亲恰好有代表字符为“C”的节点 y y 点,所以 x x fail f a i l 就为 y y

一句话:设当前节点上的字母为C,沿着它父亲节点的fail指针走,直到走到一个节点,它的子结点中也有字母为C的节点

  • 如果 fa[x].fail f a [ x ] . f a i l 的儿子节点中没有 x x 点代表字符相同的呢?
    那就继续向 fa[x].fail f a [ x ] . f a i l fail f a i l 跳!

好像是要继续向 fail f a i l 跳啊跳,但其实不用,搞 fail f a i l 的时候有个小 trick t r i c k
就是把节点 x x 没有出现过的儿子节点,直接指向 x.fail x . f a i l 的字符相同的儿子(不判断 x.fail x . f a i l 的那个儿子是否出现过)
对于每个 x x 都如此做, fail f a i l 自然而然就建好了

这样的话,每个节点的父亲的 fail f a i l 一定是已经弄出来的了,就可以用 bfs b f s 建好整个 AC A C 自动机
时间复杂度 O(n) O ( n )

code

void build_fail()
{
    while (!que.empty())que.pop();
    fail[root]=root;
    fo(i,0,25)
    {
        if (next[root][i]==-1)next[root][i]=root;
        else
        {
            fail[next[root][i]]=root;
            que.push(next[root][i]);
        }
    }
    while (!que.empty())
    {
        int now=que.front();
        que.pop();
        fo(i,0,25)
        {
            if (next[now][i]==-1)next[now][i]=next[fail[now]][i];
            else
            {
                fail[next[now][i]]=next[fail[now]][i];
                que.push(next[now][i]);
            }
        }
    }
}

AC自动机的匹配

比如现在给你一个串ushers,有heshehishers四个单词
现在问你每个单词出现了多少次

先把AC自动机建出来
《AC自动机、后缀数组(SA)学习小记(填坑ing)》

从主串ushers开始,一位一位匹配,具体如下

  • root r o o t 没有代表 u u 的儿子,跳到 root r o o t fail f a i l root r o o t 本身),处理下一位

  • root r o o t 有代表 s s 的儿子,跳到 3 3 号节点,处理下一位

  • 3 3 号节点有代表 h h 的儿子,跳到 4 4 号节点,处理下一位

  • 4 4 号节点有代表 e e 的儿子,跳到 5 5 号节点(此时可以把she的答案 +1 + 1 ),处理下一位

  • 5 5 号节点没有代表 r r 的儿子,跳到 5 5 号节点的 fail f a i l 2 2 号节点,此时可以把he的答案 +1 + 1

  • 2 2 号节点有代表 r r 的儿子,跳到 8 8 号节点,处理下一位

  • 8 8 号节点有代表 s s 的儿子,跳到 9 9 号节点(此时可以把hers的答案 +1 + 1 ),匹配完毕

匹配和建 fail f a i l 的思想是一样的

  • 就是失配时不断向当前节点的 fail f a i l 跳直到匹配成功

code

后缀数组(SA)

没了我才不学SAM

本人版权意识薄弱……

SAM那个人不人鬼不鬼的自动机我才懒得学
反正有AC自动机和SA就能做几乎所有的字符串题目啦
我好像还是连KMP都不会……

章节检索

点赞