AC自动机入门和简单应用

前言

前置知识:

  1. Trie的构建和简单应用
  2. KMP的思想

概念

构建

AC自动机实际上是在Trie中加入了fail指针的概念。

S ( i ) S(i) S(i)表示节点 i i i表示的字符串, S u f ( S ) \mathrm{Suf}(S) Suf(S)表示字符串 S S S的所有后缀(除去自己)组成的集合,那么一个 f a i l \mathrm{fail} fail指针代表的内容用形式化表示就是:
f a i l ( i ) = arg ⁡ max ⁡ S ( j ) ∈ S u f ( S ) , j ∈ V { ∣ S ( j ) ∣ } \mathrm{fail}(i)=\arg\max\limits_{S(j)\in \mathrm{Suf(S)}, j\in V}\{ |S(j)|\} fail(i)=argS(j)Suf(S),jVmax{S(j)}
特别的,如果没有任何一个满足条件的没有任何一个节点所代表的字符串是 S ( j ) S(j) S(j)的后缀,那么 f a i l ( i ) = R O O T \mathrm{fail}(i)=\mathrm{ROOT} fail(i)=ROOT

用人话来讲,就是从开头开始去掉尽可能少的字母,但是至少去除一个字母,使得所得到的新字符串依然在Trie中。

那么我们可以得到,从一个节点 u u u开始,沿着 f a i l \mathrm{fail} fail指针转跳,那么一定可以得到所有的,已经存入了Trie中的后缀。

那么显然可以得到下面的构造代码:

//ROOT = 0
queue<int> q;
void build_fail() {
    for (int i = 0; i < 26; i++) 
        if (nd[0].ch[i]) q.push(nd[0].ch[i]), nd[nd[0].ch[i]].fail = 0;
        else nd[0].ch[i] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = 0; i < 26; i++) {
            if (nd[u].ch[i])
                q.push(nd[u].ch[i]), 
                nd[nd[u].ch[i]].fail = nd[nd[u].fail].ch[i];
            else nd[u].ch[i] = nd[nd[u].fail].ch[i];
        }
    }
}

简单应用

单纯是AC自动机

[模板]AC自动机(加强版)

给定一个文本串和 N N N个模式串,求出在文本串中出现最多的模式串集合以及最大的出现次数。

根据前面得到了理论,当我们从左往右扫描文本串的时候,如果当前位置对应的AC自动机节点为 u u u,那么沿着 f a i l \mathrm{fail} fail进行转跳,将沿途所有节点的计数变量cnt都加 1 1 1

代码

#include <bits/stdc++.h>

using namespace std;

struct TreeNode { int fail, end_id, son[27]; } node[1000005];
struct Note {
    int id, cnt;
    friend bool operator < (Note a, Note b) {
        return (a.cnt != b.cnt ? a.cnt > b.cnt : a.id < b.id);
    } 
} note[155];

int N, tot_node;
string T[155], S;

void insert(string& str, int ind) { ... }
void build_fail() { ... }

void AC_match() {
    int l = S.length(), u = 0;
    for (int i = 0; i < l; i++) {
        u = node[u].son[S[i] - 'a'];
        for (int j = u; j; j = node[j].fail)
            note[node[j].end_id].cnt++;
    }
}

int main() {
    while (scanf("%d", &N), N) {
        for (int i = 0; i <= tot_node; i++) 
            memset(node[i].son, 0, sizeof(node[i].son)), node[i].end_id = node[i].fail = 0;
        tot_node = 0;
        for (int i = 1; i <= N; i++) 
            cin >> T[i], insert(T[i], i), note[i].id = i, note[i].cnt = 0;
        node[0].fail = 0;
        cin >> S;
        build_fail();
        AC_match();
        sort(note + 1, note + 1 + N);
        cout << note[1].cnt << endl << T[note[1].id] << endl;
        for (int i = 2; i <= N; i++)
            if (note[i].cnt == note[i - 1].cnt) cout << T[note[i].id] << endl;
            else break;
    }
    return 0;
}

[USACO15FEB]Censoring G

并不想概括题面,请大佬们自行点击链接查看。

可以发现题面中的一个很好的性质:给定的模式串不可能存在一个串是另一个的子串。

可以考虑用栈维护答案。同时记录扫描到文本串第 i i i个位置时对应的AC自动机中的哪一个节点。

当我们从左往右扫文本串,到达位置 p p p,当到达有某个节点 u u u,如果 S ( u ) S(u) S(u)是某一个模式串,那么我们可以将 S ( u ) S(u) S(u)从栈中直接弹出(如果是手写栈的话可以直接s-=len[u]),接着将当前所在的节点设为当位置为 p − l e n [ u ] p - len[u] plen[u]对应的节点。

好了,问题来了,为什么我们不像先前那样将 S ( u ) S(u) S(u)的所有存在与AC自动机中的后缀都看一遍,,看看是否在这些后缀中是否有模式串呢?

因为没有必要,首先节点 u u u存在的充要条件就是以节点 u u u为根的Trie子树中存在一个模式串,而很显然,这些模式串中任何一个,假设为 S S S,它的其中子串一定包含 S ( u ) S(u) S(u)的所有后缀,假设 S ( u ) S(u) S(u)所有存在于AC自动机中的后缀中有一个模式串 T T T,那么 T T T一定是 S S S的子串,和题意矛盾,所以只需要看 S ( u ) S(u) S(u)是不是模式串就好了。

代码(另一种码风):

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 5;
queue<int> q;
struct ACm {
    struct ACnode { int son[26], to[26], fail, end, len; } nd[maxn];
    int nd_tot, rt;
    ACm() { nd_tot = rt = 1; }
    
    void insert(char* str) {
        int len = strlen(str), u = rt;
        for (int i = 0; i < len; i++) {
            int ch = str[i] - 'a';
            if (!nd[u].son[ch])
                nd[u].son[ch] = ++nd_tot, nd[nd[u].son[ch]].len = nd[u].len + 1;
            u = nd[u].son[ch];
        }
        nd[u].end = true;
    } 
    void getfail() {
        for (int i = 0; i < 26; i++)
            if (nd[rt].son[i])
                nd[nd[rt].to[i] = nd[rt].son[i]].fail = rt,
                q.push(nd[rt].to[i]);
            else nd[rt].to[i] = rt;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int i = 0; i < 26; i++)
                if (nd[u].son[i])
                    nd[nd[u].to[i] = nd[u].son[i]].fail = nd[nd[u].fail].to[i],
                    q.push(nd[u].to[i]);
                else nd[u].to[i] = nd[nd[u].fail].to[i];
        }
    }
} A;

char S[maxn], T[maxn], ANS[maxn];
int pos[maxn], ans_len;
int main() {
    int n, S_len;
    scanf("%s\n%d", S+1, &n), S_len = strlen(S+1);
    while (n--) scanf("%s", T), A.insert(T);
    A.getfail(), pos[0] = A.rt;
    for (int i = 1, u = A.rt; i <= S_len; i++) {
        char ch = S[i] - 'a';
        u = A.nd[u].to[ch], pos[++ans_len] = u, ANS[ans_len] = S[i];
        if (A.nd[u].end) 
            ans_len -= A.nd[u].len, u = pos[ans_len];
    }
    for (int i = 1; i <= ans_len; i++) putchar(ANS[i]);
    putchar('\n');
    return 0;
}

AC自动机+DP

[USACO12JAN]Video Game G

设定字符集为 Σ = { ′ A ′ , ′ B ′ , ′ C ′ } \Sigma = \{\mathrm{'A','B','C'}\} Σ={A,B,C},给定 N N N个模式串,可能会有重复,现在需要构建一个长度为 k k k的文本串,使得这个文本串中模式串出现的次数尽可能多,输出最大的模式串出现次数。

首先构建一个AC自动机,其中每个节点额外记录下这个节点 u u u代表的字符串中 S ( u ) S(u) S(u)代表的模式串数量,加上 S u f ( S ( u ) ) \mathrm{Suf}(S(u)) Suf(S(u))中所有的模式串个数,设为 w ( u ) w(u) w(u)

f ( i , j ) f(i,j) f(i,j)表示已经构建出了长度为 i i i的文本串,所在的节点为 j j j,那么可以使用填表法进行计算:
f ( i + 1 , s o n ( j , k ) ) ⟵ f ( i , j ) + w ( s o n ( j , k ) ) f(i+1,\mathrm{son}(j,k))\longleftarrow f(i,j)+w(\mathrm{son}(j,k)) f(i+1,son(j,k))f(i,j)+w(son(j,k))
那么最终的答案为
a n s = max ⁡ u ∈ V { f ( k , u ) } ans=\max_{u\in V}\{f(k,u)\} ans=uVmax{f(k,u)}
⇒ \Rightarrow 代码

[JSOI2007]文本生成器

给定一堆模式串,要求构建一个文本串,长度为 m m m,满足该文本串中至少包含一个模式串,问构造方案数。

显然也是一个 d p dp dp,有两种方法:

idea 1

f ( i , j , f l ) f(i,j,fl) f(i,j,fl)表示已经构建出长度为 i i i的字符串,所在节点为 j j j,当前是否已经包含一个模式串,那么可以得到:
f ( i + 1 , s o n ( j , k ) , f l   o r   s o n ( j , k ) . f l ) ⟵ f ( i , j , f l ) f(i+1,\mathrm{son}(j,k), fl~\mathrm{or}~\mathrm{son}(j,k).fl)\longleftarrow f(i,j,fl) f(i+1,son(j,k),fl or son(j,k).fl)f(i,j,fl)
其中 u . f l u.fl u.fl表示节点 S ( u ) S(u) S(u)及其有后缀中是否包含至少一个模式串。

那么最终的答案为
a n s = ∑ u ∈ V f ( m , u , 1 ) ans=\sum_{u\in V} f(m,u,1) ans=uVf(m,u,1)
⇒ \Rightarrow 代码

idea 2

考虑更简单的容斥。

f ( i , j ) f(i,j) f(i,j)表示长度为 i i i,所在节点为 j j j,文本串中不包含任何一个模式串的方案数,那么可以得到:
f ( i + 1 , s o n ( j , k ) ) ⟵ f ( i , j ) × [ s o n ( j , k ) . f l = 0 ] f(i+1,\mathrm{son}(j,k))\longleftarrow f(i,j) \times [\mathrm{son}(j,k).fl=0] f(i+1,son(j,k))f(i,j)×[son(j,k).fl=0]
那么最终答案为:
a n s = ∣ Σ ∣ m − ∑ u ∈ V f ( m , u ) ans = |\Sigma|^m - \sum_{u\in V} f(m,u) ans=ΣmuVf(m,u)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值