前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >P5357「【模板】AC自动机(二次加强版)」

P5357「【模板】AC自动机(二次加强版)」

作者头像
hotarugali
发布2022-03-02 20:18:12
5680
发布2022-03-02 20:18:12
举报
文章被收录于专栏:hotarugaliの技术分享

1. 题目

题目链接:P5357「【模板】AC自动机(二次加强版)」

题目描述

给你一个文本串 Sn 个模式串 T_{1..n},请你分别求出每个模式串 T_i​ S 中出现的次数。

输入格式

第一行包含一个正整数 n表示模式串的个数。

接下来 n 行,第 i行包含一个由小写英文字母构成的字符串 T_i​

最后一行包含一个由小写英文字母构成的字符串 S

数据不保证任意两个模式串不相同。

输出格式

输出包含 n行,其中第 i行包含一个非负整数表示 T_iS中出现的次数。

输入输出样例

输入 #1
代码语言:javascript
复制
5
a
bb
aa
abaa
abaaa
abaaabaa
输出 #1
代码语言:javascript
复制
6
0
3
2
1

说明/提示

1 \leq n \leq 2 \times 10^5T_{1..n}的长度总和不超过2 \times 10^5S 的长度不超过 2 \times 10^6

2. 题解

分析

  • 普通的查询显然不行(TLE 一片),于是需要考虑如何优化普通的查询。普通的查询导致 TLE 主要原因在于跳 fail指针时递归的跳,对于类似aaaa \cdots aaaa 的字符串相当于每向前查找一个字符就需要递归跳 fail 指针,而每次跳 fail 只导致深度减 1,最终导致最坏的时间复杂度为 O(n*m)(其中 n 为主串长度,m为模式串长度)。
  • 由于整个字典树是确定的,fail 指针也是确定的,就是说:一个结点如果被更新了,那么字典树中能够匹配到该节点对应字符串的真后缀的结点都是确定的(即递归跳 fail 到达的结点),在递归跳 fail 过程中也一定会被更新。既然如此于是我们可以不用那么着急更新所有结点,而是考虑对匹配到最长串的结点打上标记,最后处理完后统一递归跳 fail 更新所有能够匹配到的结点。
  • 匹配完整个主串后,所有匹配到的最长串的结点都被打上了标记。那此时应该从那些结点开始递归跳 faill 指针呢?注意到,递归跳 fail指针的过程本质上是从树的叶结点走到根结点的过程,这里的树指的是依靠 fail指针构建的有向树,根结点就是字典树的根结点(因为fail[0] = 0)。于是,对于 fail指针构建的有向树而言,其叶结点的入度为 0,出度为 1(一个结点的 fail指针指向的位置是固定且唯一的),而我们首先要处理的就是所有叶结点,然后才是叶结点指向的父结点,即将父结点的所有入边关联的子结点处理完后才处理父结点,以此类推,直到根结点,而这正是拓扑排序的顺序。

根据这种思路优化后的最坏复杂度降到了 O(n+m)

代码

代码语言:javascript
复制
#include <bits/stdc++.h>
using namespace std;

// AC 自动机
struct Automaton {
    #ifndef _AUTOMATON_
    #define ll int
    #define MAXN 400005
    #define MAXCHAR 26
    #endif
    ll cnt; 
    ll trie[MAXN][MAXCHAR];
    ll exist[MAXN];
    ll fail[MAXN];
    ll mark[MAXN];          // 模式串编号
    ll in[MAXN];            // 入度
    Automaton(): cnt(0) {}
    // 插入字符串
    void insert(char *str, ll i) {
        ll p = 0;
        for(ll i = 0; str[i]; ++i) {
            ll c = str[i] - 'a';
            if(!trie[p][c]) {
                trie[p][c] = ++cnt;
            }
            p = trie[p][c];
        }
        mark[i] = p;
    }
    // 构建失配指针
    void build() {
        queue <ll> q;
        for(ll i = 0; i < MAXCHAR; ++i) {
            if(trie[0][i]) {
                q.push(trie[0][i]);
            }
        }
        // BFS 
        while(q.size()) {
            ll p = q.front();
            q.pop();
            for(ll i = 0; i < MAXCHAR; ++i) {
                if(trie[p][i]) {
                    fail[trie[p][i]] = trie[fail[p]][i];
                    ++in[trie[fail[p]][i]];
                    q.push(trie[p][i]);
                } else {
                    trie[p][i] = trie[fail[p]][i];
                }
            }
        }
    }
    // 匹配主串
    void match(char *str) {
        ll p = 0;
        for(ll i = 0; str[i]; ++i) {
            ll c = str[i] - 'a';
            ll u = trie[p][c];
            ++exist[u];
            p = trie[p][c];
        }
    }
    // 计算所有模式串出现次数
    void solve() {
        queue <ll> q;
        for(ll i = 1; i <= cnt; ++i) {
            if(!in[i])  q.push(i);
        }
        while(q.size()) {
            ll p = q.front();
            q.pop();
            if(fail[p]) {
                exist[fail[p]] += exist[p];
                --in[fail[p]];
                if(!in[fail[p]])    q.push(fail[p]);
            }
        }
    }
};


int main()
{
    ll n;
    static char str[MAXN*10];
    static Automaton ac;
    scanf("%d", &n);
    for(ll i = 0; i < n; ++i) {
        scanf("%s", str);
        ac.insert(str, i);
    }
    scanf("%s", str);
    ac.build();
    ac.match(str);
    ac.solve();
    for(ll i = 0; i < n; ++i) {
        printf("%d\n", ac.exist[ac.mark[i]]);
    }
    return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 题目
    • 题目描述
      • 输入格式
        • 数据不保证任意两个模式串不相同。
      • 输出格式
        • 输入输出样例
          • 输入 #1
          • 输出 #1
        • 说明/提示
        • 2. 题解
          • 分析
            • 代码
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档