[AC自动机][fail树] bzoj3172 单词

本文介绍了一种利用AC自动机解决单词频率统计问题的方法。通过构建AC自动机及fail树,实现对大量文本中重复单词的有效计数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

@(ACM题目)[字符串, AC自动机,fail树]

Description

某人读论文,一篇论文是由许多单词组成。但他发现一个单词会在论文中出现很多次,现在想知道每个单词分别在论文中出现多少次。

Input

第一个一个整数N,表示有多少个单词,接下来N行每行一个单词。每个单词由小写字母组成,N<=200,单词长度不超过10^6

Output

输出N个整数,第i行的数字表示第i个单词在文章中出现了多少次。

Sample Input

3
a
aa
aaa

Sample Output

6
3
1

题目分析

为了叙述方便,将从根节点到一个结点u所代表的字符串称为u的路径字符串,记为su

首先将所有单词加入AC自动机,并统计每个结点u的路径字符串在单词表的中的单词前缀中出现的次数cntu

对Trie树中的每个结点u,将它与v=failu连一条边,方向为vu。由于每个点都有且仅有一个fail指针,且形成的图是连通的,所以这是一棵树,将它称为“fail 树”。
在这棵树中,对于每对父子(par,son)sparsson的后缀,sparsson中出现。所以,对于一个结点u,要统计su出现的次数,只需要统计在fail树中,子树uΣcnt即可,这一统计过程一遍dfs即可实现。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

namespace ACautomaton
{
    const int maxn = 1e6 + 5;
    const int maxm = 26;
    int ch[maxn][maxm];//ch[i][c]代表结点i的c孩子;初始有一个根节点,代表空字符串
//    int val[maxn];//val为正代表这是一个模式串单词结点
    int fail[maxn];//suffix link,代表当前路径字符串的最大前缀
//    int last[maxn];//output link, 上一个单词结点
    int tot;//Trie树中结点总数

    int sz;//单词总数
    int pos[205];//pos[i]为单词i在trie树中的对应结点

    int cnt[maxn];//统计该结点的路径字符串出现的次数

    int to[maxn], nxt[maxn], head[maxn], nume;//fail树

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

        nume = 0;//零条边
        memset(head, 0xff, sizeof head);
    }

    void addEdge(int u, int v)
    {
        to[nume] = v;
        nxt[nume] = head[u];
        head[u] = nume ++;
    }

    //O(n),n为所有模式总长度
    void add(char *P, int v)//插入模式串,值为v
    {
        int u = 0;//当前结点
        int n = strlen(P);
        for(int i = 0; i < n; ++i)
        {
            int c = P[i] - 'a';
            if(!ch[u][c])//若当前结点无c孩子,则创造一个
            {
                memset(ch[tot], 0, sizeof ch[tot]);
//                val[tot] = 0;//中间结点的值为零
                ch[u][c] = tot++;
            }
            u = ch[u][c];//走向当前结点的c孩子
            ++cnt[u];
        }
        //现在走到了模式串的结尾结点
//        val[u] += v;
        pos[sz++] = u;
    }

    //O(tot)的
    void getFail()//构造fail指针和last指针
    //使用BFS,因为fail指针一定指向长度更短的字符串
    {
        queue<int> q;
        fail[0] = 0;
        //初始化队列
        for(int c = 0; c < maxm; ++c)
        {
            int u = ch[0][c];
            if(u)
            {
                fail[u] = 0;//第一层结点的fail都是根节点
//                last[u] = 0;
                addEdge(0, u);
                q.push(u);//将第一层结点加入队列
            }
        }

        //BFS
        while(!q.empty())
        {
            int cur = q.front();
            q.pop();
            for(int c = 0; c < maxm; ++c)//为cur结点的c孩子添加fail指针
            {
                int u = ch[cur][c];
                if(!u)//当前结点没有c孩子
                {
                    ch[cur][c] = ch[fail[cur]][c];//沿fail往上找,因为fail指针指向的还是这个后缀
                    continue;
                }
                q.push(u);//c孩子入队
                int v = fail[cur];
                while(v && !ch[v][c]) v = fail[v];//若后缀结点无c孩子,就沿fail指针一直网上找
                fail[u] = ch[v][c];//给c孩子添加fail指针
                addEdge(fail[u], u);
                //若c孩子的fail指针指向模式串结点,则c孩子的last指向fail指针位置即可,因为这就是最长的
                //否则指向fail指针指向的结点的last即可
//                if(val[fail[u]]) last[u] = fail[u];
//                else last[u] = last[fail[u]];
            }
        }
    }

    void dfs(int cur)
    {
        for(int i = head[cur]; ~i; i = nxt[i])
        {
            dfs(to[i]);
            cnt[cur] += cnt[to[i]];
//            cout << "cur = " << cur << "; son = " << to[i] << endl;
        }
    }
};

const int maxn = 1e6 + 5;
char s[maxn];

int main()
{
    int n;
    cin >> n;
    ACautomaton::init();
    for(int i = 0; i < n; ++i)
    {
        scanf("%s", s);
        ACautomaton::add(s, 1);
    }
    ACautomaton::getFail();
    ACautomaton::dfs(0);
    for(int i = 0; i < n; ++i)
    {
        using namespace ACautomaton;
        printf("%d\n", cnt[pos[i]]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值