数据结构-AC自动机

版权声明:转载请注明出处。 https://blog.csdn.net/baidu_38304645/article/details/83213087

在模式匹配问题中,如果模板有很多个,KMP算法就不太合适了。因为每次查找一个模板,都要遍历整个文本串。可不可以只遍历一次文本串呢?可以,方法是把所有模板建成一个大的状态转移图(称为Aho-Corasick自动机,简称AC-自动机),而不是每个模板各建一个状态转移图。

注意到KMP的状态转移图是线性的字符串加上失配边组成的 ,不难想到AC自动机是Trie加上失配边组成的。

如果已经构造好AC自动机,其匹配算法几乎和KMP是一样的。

主串T:sjeushashehiahersahis
模板串:
he
she
his
hers

输入:主串T,模板串的数目b,各个模板串以及其权值。

输出:主串中出现的所有模板串在Trie树中所代表的结点j,以及其权值。

运行结果(注意“he”串出现了两次):

存储数据结构:

 int ch[maxnode][sigma_size];
    int val[maxnode];
    int sz;
    int last[maxnode];
    int f[maxnode];

初始化以及辅助函数:

void init()
    {
        sz = 1;
        memset(ch[0], 0, sizeof(ch[0]));
    }

    int idx(char c)
    {
        //字符c的编号
        return c - 'a';
    }

插入字符串s,附加信息为v 注意v必须非0 因为0代表“本结点不是单词结点”。

void insert(const char *s, int v)
    {
        int u, n, i, c;

        u = 0;
        n = strlen(s);
        for(i = 0; i < n; i++)
        {
            c = idx(s[i]);
            if(!ch[u][c])                               //结点不存在
            {
                memset(ch[sz], 0, sizeof(ch[sz]));
                val[sz] = 0;                            //中间结点的附加信息为0
                ch[u][c] = sz++;                        //新建结点
            }
            u = ch[u][c];
        }

        val[u] = v;                                     //字符串的最后一个字符附加信息为v
    }

在文本串中找模板。

 //在文本串中找模板
    void find(char* T)
    {
         int i, j, n, c;

         j = 0;                                       //当前结点编号,初始为根结点
         n = strlen(T);
         for(i = 0; i < n; i++)                       //文本串当前指针
         {
             c = idx(T[i]);
             while(j && !ch[j][c])                    //顺着失配边走,直到可以匹配
                j = f[j];
             j = ch[j][c];
             if(val[j])                              // 找到了
                 print(j);
             else if(last[j])
                 print(last[j]);
         }
    }

递归打印以结点j结尾的所有字符串.

//递归打印以结点j结尾的所有字符串
    void print(int j)
    {
        if(j)
        {
            printf("%d %d\n", j, val[j]);
            print(last[j]);
        }
    }

代码中出现了一个last数组。下面来解释一下,和Trie一样,我们认为所有val[j]>0的结点j都是单词结点,反之亦然。但和Trie不同的是,同一个结点可能对应多个字符串的结尾。

所以当找到一个模板后,应该顺着失配指针往回走,看看有没有其他串。当然,失配指针不一定指向一个单词结点。为了提高效率,增设一个指针last[j] 表示结点j沿着失配指针往回走时,遇到的下一个单词结点编号。这个last[j]在正规文献里叫做后缀链接。

计算失配函数和KMP很相近,只是把线性递归改成了按照BFS顺序递推。

//计算失配函数
    void getFail()
    {
        int i, v, u, r;
        queue<int> q;

        f[0] = 0;
        //初始化队列
        for(i = 0; i < sigma_size; i++)
        {
            u = ch[0][i];
            if(u)
            {
                q.push(u);
                f[u] = 0;
                last[u] = 0;
            }
        }
        //按bfs顺序计算失配函数
        while(!q.empty())
        {
            r = q.front();
            q.pop();
            for(i = 0; i < sigma_size; i++)
            {
                u = ch[r][i];
                if(!u)                               // 优化:可以修改为 if(!u){ch[r][c]=ch[f[r]][c];continue},
                    continue;                        // 把find函数中的语句while(j && !ch[j][c]) j = f[j]; 删除
                q.push(u);
                v = f[r];                  // 父结点的f
                while(v && !ch[v][i])
                    v = f[v];
                f[u] = ch[v][i];
                last[u] = val[f[u]] ? f[u]:last[f[u]];    //计算上一个单词结点
            }
        }
    }

由于失配过程比较复杂,要反复沿着失配边走,在实践中常常会把上述AC自动机改造一下,把所有不存在的边补上,即把计算失配函数中的语句 “if(!u) continue”改成:

if(!u) {ch[r][c]=ch[f[r][c]; continue;}

这样,就完全不需要失配函数,而是对所有的移动一视同仁。也就是说,find函数中的语句 “while(j && !ch[j][c]) j=f[j];” 可以直接完全删除。

猜你喜欢

转载自blog.csdn.net/baidu_38304645/article/details/83213087
今日推荐