AC 自动机

OI-wiki Link and bilibili Link

AC 自动机,主要用于解决多模式串(即需要求出出现次数等的串)匹配的问题,基于字典树。

大致

将模式串建到字典树上,对每个字典树上的节点求出失配指针,根据失配指针建立失配树,用失配树来维护模式串出现次数。

具体构建

建立字典树

略。

失配 fail 指针

失配指针用于辅助多模式串匹配。

节点 \(x\) 的失配指针意思是其对应字符串 \(s\) 的最长可匹配(在字典树中能找到)后缀对应字典树的哪个节点。

由于是最长可匹配后缀,那么我们可以发现,只要我们不断枚举 fail[x], fail[fail[x]] ...,就可以把其每个可匹配后缀都枚举出来。

当字典树上一个节点 \(x\) 在后接一个字符 \(c\)、转移至 \(y\) 时(假设已经求出了 \(x\) 的 fail 指针),只需要枚举每个 \(x\) 的每个可匹配后缀,判断在其后面加上 \(c\) 是否有对应的节点即可。

但这样做的复杂度似乎不太对,考虑优化。

路径压缩优化(字典图)

为了避免反复枚举一个转移 \(a \xrightarrow{c} b\),可以按照深度去求 fail。枚举 \(x\) 和后接的一个字符 \(y\) 时,如果 \(x\) 没有一个 \(y\) 转移,则记录 \(trie_{x,y}=trie_{fail_x, y}\),将路径压缩存储下来,否则就更新 \(fail_{trie_{x,y}}=trie_{fail_x,y}\)

由于这个操作改变了字典树的结构,所以也被称为建立字典图。

建立 fail 树

求出 fail 后,一般都需要用 fail 树来辅助维护答案。

顾名思义,就是以 \(fail_x\)\(x\) 的父亲搭建出的一棵树,其性质是如果 \(x\) 对应字符串被成功匹配了一次,那么其父亲也必然会被成功匹配一次,可以用求子树内权值和的方式来求出现次数。

至此,AC 自动机的前置搭建已经完成,那么就是处理匹配的问题了。

处理匹配

仍然是在字典图上面处理,求出答案后把标记打在失配树上,最后用失配树来处理答案。

假设现在匹配到了 \(x\) 这个节点,后接一个字符 \(y\),如果字典图上有这个转移则直接转移过去,否则转移到 \(trie_{fail_x,y}\) 去,每接一个字符都要记得在失配树上对应节点打上一个标记。

最后求答案即可。

Code

#include <iostream>
#include <vector>
#include <algorithm>
#define _1 (__int128)1

using namespace std;
using ll = long long;

void FileIO (const string s) {
  freopen((s + ".in").c_str(), "r", stdin);
  freopen((s + ".out").c_str(), "w", stdout);
}

const int N = 2e5 + 10;

int n, trie[N][30], ndcnt, fail[N], cnt[N], ans[N];
string s;
vector<int> id[N], g[N];
pair<int, int> dfn[N];

void Insert (string s, int x) {
  int now = 0;
  for (int i = 0; i < s.size(); i++) {
    if (!trie[now][s[i] - 'a']) trie[now][s[i] - 'a'] = ++ndcnt, dfn[ndcnt] = {i, ndcnt};
    now = trie[now][s[i] - 'a'];
  }
  id[now].push_back(x);
}

void Get (int x) {
  for (int i : g[x])
    Get(i), cnt[x] += cnt[i];
  for (int i : id[x])
    ans[i] = cnt[x];
}

signed main () {
  ios::sync_with_stdio(0), cin.tie(0);
  // FileIO("");
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> s, Insert(s, i);
  cin >> s;
  sort(dfn + 1, dfn + ndcnt + 1);
  for (int i = 1; i <= ndcnt; i++) {
    int x = dfn[i].second;
    for (int j = 0; j < 26; j++) {
      if (trie[x][j])
        fail[trie[x][j]] = trie[fail[x]][j];
      else
        trie[x][j] = trie[fail[x]][j];
    }
  }
  for (int i = 1; i <= ndcnt; i++)
    g[fail[i]].push_back(i);
  for (int i = 0, j = 0; i < s.size(); i++) {
    if (trie[j][s[i] - 'a']) j = trie[j][s[i] - 'a'];
    else j = trie[fail[j]][s[i] - 'a'];
    cnt[j]++;
  }
  Get(0);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << '\n';
  return 0;
}

例题

模板模板+稍微修改,模板还有两个弱化版。

练习题:病毒 | 阿狸的打字机 | 文本生成器

posted @ 2024-11-10 21:51  wnsyou  阅读(18)  评论(0编辑  收藏  举报