算法学习:AC自动机

【定义】

【自动机】  由 状态集 ,初始状态集 ,终止状态集 ,字母集 ,对应关系五个元素组成的结构

       可以简单的将状态集理解为结点初始状态集理解为初始点终止状态集理解为终点

                   字母集理解为一个状态能够拥有的出边的最大个数,而在自动机中,特殊的是,一个结点的所有出边必须都要存在

                   例如在AC自动机中,每个节点都必须要有26个字母的出边所指向的节点

                   对应关系,可以理解为连通的边,例:节点 u 的 ’a‘ 的出边能够到达节点 v ,这就是一组对应关系

 

 注:自动机的概念不用知道也可以学自动机,但是个人感觉理解了自动机的含义,更容易去明白他的本质

        而且后面学其他自动机也更容易理解

 

【模式串】  一个比较短的串,需要找  文本串上有多少个他

 

【文本串】   一个比较长的串,需要在  他上面有多少个模式串

 


【前置知识】

【trie树(字典树)】从根节点插入一个字符串,依次插入

【KMP】单文本 logn 复杂度内查找模式串出现次数

 

【强烈建议看最后的扩展】 

 


【解决问题】

 多个模式串匹配文本串

给定一个较长串为文本串,给定多个模式串,询问这两者的关系

(也有可能不只有一个文本串)

一般为出现次数什么的

 

AC自动机本身保存这个字符串的所有子串的相关信息

即这个子串作为模式串出现的次数,这个子串的后缀之中能够包含多少模式串

 


【算法思想】

这个自动机的优点和KMP类似,所以有的博客中也会说,这是一个树上KMP。

KMP的优点在于有next数组作为指针失配时的指针,使字符串匹配可以不需要再次查找已经查找的串

而AC自动机也有他的“next数组”,f a i l  

AC自动机中的 f a i l 指针表示的是,当当前匹配的模式串失效后,已经匹配的一半模式串的拥有最长后缀的模式串的结尾的位置

 

对于一个AC自动机,我们需要先把所有的模式串都插入一颗 trie 树

int trie[MAXN][26];
//s是需要插入的模式串
void insert(string s)
{
    int u = 0;
        //从根节点开始检索
    for (int i = 0; i < s.size(); i++)
    {
        int x = s[i] - 'a';
        if (!trie[u][x])
                //如果没有这个节点
               //需要新开拓一个节点,保存这个新的分支
        {
            trie[u][x] = ++cnt;
        }
        u = trie[u][x];
    }
    val[u]++;
        //说明这个节点是一个模式串的结尾
    return;
}

 

 然后根据我们对 f a i l 的定义去寻找f a i l 指针

 

void get_fail()
{
    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指针指向根节点
        //并且加入队列
    while (!q.empty())
    {
        int u = q.front(); q.pop();
        for (int i = 0; i < 26; i++)
            //循环查找每个字母    
                    if (trie[u][i])
            //如果存在这个节点
                        //
                        {
                                //他的 fail 就是 他父节点的 fail 的这个字母的位置
                               //因为等于是在后缀上新加了一个字母 
                fail[trie[u][i]] = trie[fail[u]][i];
                q.push(trie[u][i]);
            }
            else
                trie[u][i] = trie[fail[u]][i];
                     //如果没有
                      //向上递归一层,方便之后的查找
    }
    return;
}

 

这里应该有张图说明一下,但是我懒得画

 


 

 

【模板题】

【题目大意】给n个模式串和1个文本串,问有多少个模式串在文本串中出现过

 

【解决方法】在trie树上跑文本串

int query(string s)
{
    int u = 0, ans = 0;
       //从根节点开始依次查找
    for (int i = 0; i < s.size(); i++)
    {
        u = trie[u][s[i] - 'a'];
               //走到自己这个位置字符所在的节点
        for (int t = u; t && ~val[t]; t = fail[t])
               //从这个节点开始向上跳fail指针
               //查找有没有符合要求的字符串,如果是则这个字符串就会被记录
            ans += val[t], val[t] = -1;
              //-1是为了防止被重复计算
    }
    return ans;
}

 

#include<cstdio>
#include<iostream>
#include<string>
#include<queue>
using namespace std;
const int MAXN = 1000010;
int fail[MAXN],cnt;
int trie[MAXN][26];
int val[MAXN];
void insert(string s)
{
    int u = 0;
    for (int i = 0; i < s.size(); i++)
    {
        int x = s[i] - 'a';
        if (!trie[u][x])
        {
            trie[u][x] = ++cnt;
        }
        u = trie[u][x];
    }
    val[u]++;
    return;
}
void get_fail()
{
    queue<int> q;
    for (int i = 0; i < 26; i++)    
        if (trie[0][i])  fail[trie[0][i]] = 0, q.push(trie[0][i]);
    while (!q.empty())
    {
        int u = q.front(); q.pop();
        for (int i = 0; i < 26; i++)
            if (trie[u][i])
            {
                fail[trie[u][i]] = trie[fail[u]][i];
                q.push(trie[u][i]);
            }
            else
                trie[u][i] = trie[fail[u]][i];
    }
    return;
}
int query(string s)
{
    int u = 0, ans = 0;
    for (int i = 0; i < s.size(); i++)
    {
        u = trie[u][s[i] - 'a'];
        for (int t = u; t && ~val[t]; t = fail[t])
            ans += val[t], val[t] = -1;
    }
    return ans;
}
int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        string s;
        cin >> s;
        insert(s);
    }
    get_fail();
    
    string s;
    cin >> s;
    printf("%d", query(s));
    return 0;
}
View Code

 


【扩展】

 

这一步扩展其实才是AC自动机的精髓所在,也是比赛中比较常用的方法

就像是网络流不可能直接给图找最大流,而是通过建图的方式考察对算法的理解

AC自动机也有同样的情况

那就是通过抽离 fail 指针,建 fail 树,并且对其进行一系列操作完成任务

 

 

这不就是AC自动机抽离fail指针建可持久化线段树么

 

讲解在另外一道题里面

【NOI 2011】 阿狸的打字机

 

posted @ 2019-08-08 20:56  rentu  阅读(314)  评论(0编辑  收藏  举报