AC自动机与Trie树

3 篇文章 0 订阅
2 篇文章 0 订阅

AC自动机的前置学习需求是Trie树和KMP算法,AC自动机相当于二维的KMP算法。

应用场景:求一堆字符串在一个文本中出现的次数

文章目录

AC自动机

#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = 1000010;
int n;
int tr[N * S][26]; // Trie树的数组
int cnt[N * S];    //单词出现的次数
int ne[N * S];     //回溯的位置
int idx;
char str[M];

void insert()
{
    int p = 0;
    for (int i = 0; str[i]; i++)
    {
        int t = str[i] - 'a';
        if (!tr[p][t])
            tr[p][t] = ++idx;
        p = tr[p][t];
    }
    cnt[p]++;
}
void build()
{
    queue<int> q;
    for (int i = 0; i < 26; i++)
        if (tr[0][i])
            q.push(tr[0][i]);

    while (!q.empty())
    {
        int t = q.front();
        q.pop();
        for (int i = 0; i < 26; i++)
        {
            int p = tr[t][i];
            int j = ne[t];
            if (!tr[t][i])
                continue;

            while (j && !tr[j][i])
                j = ne[j];
            if (tr[j][i])
                j = tr[j][i];
            ne[p] = j;
            q.push(p);
        }
    }
}
int query()
{
    int res = 0;
    for (int i = 0, j = 0; str[i]; i++)
    {
        int t = str[i] - 'a';
        while (j && !tr[j][t])
            j = ne[j];
        if (tr[j][t])
            j = tr[j][t];

        /*
        还需要加上该串的最大后缀(相当于是前缀, 这个后缀是由更小的后缀转移过来的)
        如果该前缀是单词, 那么ne[p]是尾节点编号, cnt[ne[p]] > 0, 加上就行, 不是单词+0, 就相当于没加.
        */
        int p = j;
        while (p && cnt[p] != -1)
        {
            res += cnt[p];
            cnt[p] = -1;
            p = ne[p];
        }
    }
    return res;
}
int main()
{
    memset(tr, 0, sizeof tr);
    memset(cnt, 0, sizeof cnt);
    memset(ne, 0, sizeof ne);
    idx = 0;
    int n;
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        scanf("%s", str);
        insert();
    }
    build();
    scanf("%s", str);
    printf("%d\n", query());
    return 0;
}

Trie图

原来需要一下一下跳直到找到可匹配前缀, 现在通过累积维护, 一层一层更新, 可以一步直接找到可匹配前缀的下标.

#include <bits/stdc++.h>
using namespace std;
const int N = 10010, S = 55, M = 1000010;

int n;
int tr[N * S][26]; // Trie树的数组
int cnt[N * S];    //单词出现的次数
int ne[N * S];     //回溯的位置
int idx;
char str[M];

void insert()
{
    int p = 0;
    for (int i = 0; str[i]; i++)
    {
        int t = str[i] - 'a';
        if (!tr[p][t])
            tr[p][t] = ++idx;
        p = tr[p][t];
    }
    cnt[p]++;
}

void build()
{
    queue<int> q;
    for (int i = 0; i < 26; i++)
        if (tr[0][i])
            q.push(tr[0][i]);//将第二层所有出现了的字母扔进队列
    while (!q.empty())
    {
        int t = q.front();
        q.pop();
        for (int i = 0; i < 26; i++)
        {
            int p = tr[t][i]; //一层一层的更新过来维护每个节点的最近的可匹配前缀编号, 由一个一个的跳, 变成了一步到位,t的儿子都没有i,那匹配的儿子里面的i必然是匹配不到了.空的还是要赋起走tr[t][i] = tr[ne[t]][i], 以便后面的不空的用tr[ne[t]][i]赋值
            if (!p)
                tr[t][i] = tr[ne[t]][i]; // 形成了一个链式关系, 当前节点的最大前缀一定是由前面的转移过来的
            else
                ne[p] = tr[ne[t]][i],
                q.push(
                    p); // 可能j(ne[t])指针的儿子节点中没有i,但是tr[ne[t]][i]的值为离他最近的最大前缀坐标.(过滤掉了ne的子节点也没有i这个儿子节点的情况)
        }
    }
}
int query()
{
    int res = 0;
    for (int i = 0, j = 0; str[i]; i++)
    {
        int t = str[i] - 'a';
        j = tr[j][t]; // 将原来需要跳多次, 通过维护最大前缀坐标, 就可以一次性跳转.
        int p = j;
        while (p && cnt[p] != -1)
        {
            res += cnt[p];
            cnt[p] = -1;
            p = ne[p];
        }
    }
    return res;
}
int main()
{
    memset(tr, 0, sizeof tr);
    memset(cnt, 0, sizeof cnt);
    memset(ne, 0, sizeof ne);
    idx = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
    {
        scanf("%s", str);
        insert();
    }
    build();
    scanf("%s", str);
    printf("%d\n", query());
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值