AC自动机学习笔记

· · 个人记录

基础的AC自动机我不讲,自行学习

这篇文章具体讲一下各种题目要求的实现和AC自动机上dp(画大饼)

(以下代码中 AC[a].vis[b] 表示AC自动机中的 a 节点的子节点中代表字符 b 的节点编号)

Part 1 AC自动机题目常见的问法

1.某些字符串是否出现(雾

说好的不讲模板,自行学习

2.某(些)字符串出现次数

insert 即插入函数和 query 求值函数修改即可

代码:

//插入函数
inline void insert(string s,int idd)
{
    int l=s.length();
    int now=0;
    for(int i=0;i<l;i++)
    {
        if(AC[now].vis[s[i]-'a']==0)
        AC[now].vis[s[i]-'a']=++cnt;
        now=AC[now].vis[s[i]-'a'];
    }
    //这两行改就行了,这里处理了重复串情况
    if(AC[now].end==0)AC[now].end=idd;
    id[idd]=AC[now].end;
    return;
}

//求值函数
//朴素版
inline void query(string s)
{
    int l=s.length();
    int now=0;
    for(int i=0;i<l;i++)
    {
        now=AC[now].vis[s[i]-'a'];
        for(int t=now;t;t=AC[t].fail)
        {
            if(AC[t].end)num[id[AC[t].end]]++;
        }
    }
    return;
}
//拓扑排序优化版
inline void query(string s)
{
    int l=s.length();
    int now=0;
    for(int i=0;i<l;i++)
    {
        now=AC[now].vis[s[i]-'a'];
        vis[now]++;
    }
    topo();
    for(int i=1;i<=cnt;i++)
    {
        if(AC[i].end)ans[AC[i].end]=vis[i];
    }
}

拓扑排序优化背板子就好了(逃

3.(其实是 2 的一个子问题)某个(些)字符串出现次数最多

这个在 2 的基础上加些处理就好了

4.还是讲下拓扑优化吧

就是一个拓扑排序的板子......吧

在求 fail 指针时预处理,然后掏出下面的板子:

inline void topo()
{
    queue<int>q;
    for(int i=0;i<=cnt;i++)
    {
        if(!in[i])q.push(i);
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[AC[u].fail]+=vis[u];
        if(--in[AC[u].fail]==0)
        {
            q.push(AC[u].fail);
        }
    }
    return;
}

fail 时的预处理(核心代码):

while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++)
        {
            if(AC[u].vis[i]!=0)
            {
                AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
                in[AC[AC[u].fail].vis[i]]++;//只多了这一行
                q.push(AC[u].vis[i]);
            }
            else AC[u].vis[i]=AC[AC[u].fail].vis[i];
        }
    }

然后用 query 的那个带优化的板子就行了

Part 1.5 fail

因为学AC自动机上dp时发现 fail 树是一个很重要的东西,故略提一下

思考 fail 指针的本质:跳转至与当前串有相同后缀的串的跳转指针

(语文不好解释起来很拗口,可以自行 bdfs )

在构建 fail 指针时,我们将 Trie 树补全成为了一张有向图,可以叫 Trie

那么就可以在AC自动机上进行图上操作

例题:P2444

这道题的安全代码在AC自动机上匹配永远无法匹配成功(如果匹配成功就包含了病毒),所以就会在匹配中一直在AC自动机中跳

所以就是在有向图中求一个环,使得环上不存在病毒串的末尾节点, 大法师即可

代码:

#include<bits/stdc++.h>
using namespace std;
const int M=3e4+5;
int n,cnt;
string s;
bool in[M],vis[M];
struct node{
    int vis[2];
    int end;
    int fail;
}AC[M];
inline void insert(string s)
{
    int l=s.length();
    int now=0;
    for(int i=0;i<l;i++)
    {
        if(AC[now].vis[s[i]-'0']==0)
        AC[now].vis[s[i]-'0']=++cnt;
        now=AC[now].vis[s[i]-'0'];
    }
    AC[now].end++;
    return;
}
inline void get_fail()
{
    queue<int>q;
    for(int i=0;i<2;i++)
    {
        if(AC[0].vis[i]!=0)
        {
            AC[AC[0].vis[i]].fail=0;
            q.push(AC[0].vis[i]);
        }
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<2;i++)
        {
            if(AC[u].vis[i]!=0)
            {
                AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
                q.push(AC[u].vis[i]);
            }
            else AC[u].vis[i]=AC[AC[u].fail].vis[i];
        }
        AC[u].end+=AC[AC[u].fail].end;//如果一个点的fail不能在环上,那么它也不能在环上
    }
    return;
}
inline void dfs(int p)//求有向图中的环
{
    if(in[p]){cout<<"TAK";exit(0);}//找到直接结束
    if(vis[p]||AC[p].end)return;
    in[p]=vis[p]=1;
    dfs(AC[p].vis[0]);
    dfs(AC[p].vis[1]);
    in[p]=0;
    return;
}
int main()
{
//  freopen(".in","r",stdin);
//  freopen(".out","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        insert(s);
    }
    get_fail();
    dfs(0);
    cout<<"NIE";
    return 0;
}

Part 2 AC自动机上dp

AC自动机上的dp一般是二维的

选一题经典题:[P4052](https://www.luogu.com.cn/problem/P4052) 这题要求出不可读的串,然后用所有串减不可读串得到可读串 所有串是 $26^m$ ,问题在于如何求不可读串 考虑dp 对读入的字符串建AC自动机,如果一个点是一个单词结尾或其 $fail$ 是一个单词结尾,那么就不能更新答案,反之则可以 AC自动机上dp的经典思想是:用父亲更新儿子 统计方案数的问题不难得出方程:$f[i+1][k]+=f[i][j]

那么最终的答案就是:26^m - \sum f[m][i]

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=105,M=6005,mod=1e4+7;
int n,m,cnt,f[N][M],ans=1,sum;
string s;
struct node{
    int vis[26];
    int end;
    int fail;
}AC[M];
inline void insert(string s)
{
    int l=s.length();
    int now=0;
    for(int i=0;i<l;i++)
    {
        if(AC[now].vis[s[i]-'A']==0)
        AC[now].vis[s[i]-'A']=++cnt;
        now=AC[now].vis[s[i]-'A'];
    }
    AC[now].end++;
    return;
}
inline void get_fail()
{
    queue<int>q;
    for(int i=0;i<26;i++)
    {
        if(AC[0].vis[i]!=0)
        {
            AC[AC[0].vis[i]].fail=0;
            q.push(AC[0].vis[i]);
        }
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=0;i<26;i++)
        {
            if(AC[u].vis[i]!=0)
            {
                AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
                q.push(AC[u].vis[i]);
            }
            else AC[u].vis[i]=AC[AC[u].fail].vis[i];
        }
        AC[u].end+=AC[AC[u].fail].end;//参见Part 1.5
    }
    return;
}
int main()
{
//  freopen(".in","r",stdin);
//  freopen(".out","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        insert(s);
    }
    get_fail();
    f[0][0]=1;
    for(int i=0;i<=m-1;i++)
    {
        for(int j=0;j<=cnt;j++)
        {
            for(int k=0;k<26;k++)
            {
                if(AC[AC[j].vis[k]].end==0)//如果不是结尾
                (f[i+1][AC[j].vis[k]]+=f[i][j])%=mod;
            }
        }
    }
    for(int i=1;i<=m;i++)(ans*=26)%=mod;//暴力美学
    for(int i=0;i<=cnt;i++)
    {
        (sum+=f[m][i])%=mod;
    }
    cout<<(ans-sum+mod)%mod;
    return 0;
}

先写这点,还在研究矩阵快速幂优化

## Part 3 又见 $fail$ 树 AC自动机的重点竟然是 $fail$ 树!!! (好像才知道一样) $Part 1.5$ 中说了 $fail$ 树的基础,以及讲了一题 $Trie$ 图上求环,这个部分讲讲树上操作 例题:[CF163E](https://www.luogu.com.cn/problem/CF163E) 这题算是比较入门的 $fail$ 树上操作 [推荐大佬的题解](https://www.luogu.com.cn/blog/George1123/solution-cf163e) ~~当然你也可以直接看我的~~ 删除一个字符串相当于在 $fail$ 树上把这个点的权值减 $1$ ,加入则相当于加 $1$ ,而贡献会受影响的是该节点的子树(可以想想 $fail$ 树的本质或画图理解) 那么就掏个数据结构维护一下贡献 ~~,我推的线段树~~,$dfs$ 序建树 ~~各位巨佬肯定懂了,那直接看代码吧~~ ```cpp #include<bits/stdc++.h> #define ll int //抄板子不想改类型 using namespace std; const int N=1e5+5,M=1e6+5; int n,k,id[M],ed[M],a[N],cnt,tot; vector<int>v[M];//vecotr存图(不会链式前向星 string s; bool vis[N]; struct node{ int vis[26]; int fail; }AC[M]; inline void insert(string s,int idd) { int l=s.length(); int now=0; for(int i=0;i<l;i++) { if(AC[now].vis[s[i]-'a']==0) AC[now].vis[s[i]-'a']=++cnt; now=AC[now].vis[s[i]-'a']; } a[idd]=now;//记录各个字符串终止节点在fail树上位置 return; } inline void get_fail() { queue<int>q; for(int i=0;i<26;i++) { if(AC[0].vis[i]!=0) { AC[AC[0].vis[i]].fail=0; q.push(AC[0].vis[i]); } } while(!q.empty()) { int u=q.front(); q.pop(); for(int i=0;i<26;i++) { if(AC[u].vis[i]!=0) { AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i]; q.push(AC[u].vis[i]); } else AC[u].vis[i]=AC[AC[u].fail].vis[i]; } } //建树 for(int i=1;i<=cnt;i++) { v[AC[i].fail].push_back(i); } return; } inline void dfs(int p) { id[p]=++tot; for(int t:v[p]) { dfs(t); } ed[p]=tot; return; } struct nnode{ ll v,tag,l,r; }; struct Tree{ nnode Tr[4*M]; inline ll ls(ll id){return id<<1;} inline ll rs(ll id){return id<<1|1;} inline void pushup(ll id) { Tr[id].v=Tr[ls(id)].v+Tr[rs(id)].v; return; } inline void pushdown(ll id) { if(Tr[id].tag) { Tr[ls(id)].v+=(Tr[ls(id)].r-Tr[ls(id)].l+1)*Tr[id].tag; Tr[rs(id)].v+=(Tr[rs(id)].r-Tr[rs(id)].l+1)*Tr[id].tag; Tr[ls(id)].tag+=Tr[id].tag,Tr[rs(id)].tag+=Tr[id].tag; Tr[id].tag=0; } return; } inline void build(ll id,ll l,ll r) { Tr[id].l=l,Tr[id].r=r; if(l==r) { return; } ll mid=(l+r)>>1; build(ls(id),l,mid),build(rs(id),mid+1,r); pushup(id); return; } inline void modify(ll id,ll a,ll b,ll k) { if(Tr[id].l>b||Tr[id].r<a) { return; } if(a<=Tr[id].l&&Tr[id].r<=b) { Tr[id].v+=(Tr[id].r-Tr[id].l+1)*k; Tr[id].tag+=k; return; } pushdown(id); ll mid=(Tr[id].l+Tr[id].r)>>1; if(a<=mid)modify(ls(id),a,b,k); if(b>mid)modify(rs(id),a,b,k); pushup(id); return; } inline ll query(ll id,ll a,ll b) { if(Tr[id].l>b||Tr[id].r<a) { return 0; } if(a<=Tr[id].l&&Tr[id].r<=b) { return Tr[id].v; } pushdown(id); ll mid=(Tr[id].l+Tr[id].r)>>1,sum=0; if(a<=mid)sum+=query(ls(id),a,b); if(b>mid)sum+=query(rs(id),a,b); return sum; } }T; inline int query(string s) { int l=s.length(); int now=0,res=0; for(int i=0;i<l;i++) { now=AC[now].vis[s[i]-'a']; res+=T.query(1,id[now],id[now]);//加上贡献 } return res; } int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); cin>>n>>k; for(int i=1;i<=k;i++) { cin>>s; insert(s,i); } get_fail(); //大法师求dfs序 dfs(0); T.build(1,1,tot); for(int i=1;i<=k;i++) { //初始时都在集合中 vis[i]=1; T.modify(1,id[a[i]],ed[a[i]],1); } while(n--) { char ch; cin>>ch; if(ch=='+') { int tmp; cin>>tmp; if(vis[tmp])continue;//已经存在 vis[tmp]=1; T.modify(1,id[a[tmp]],ed[a[tmp]],1);//修改子树 } if(ch=='-') { int tmp; cin>>tmp; if(!vis[tmp])continue;//不存在 vis[tmp]=0; T.modify(1,id[a[tmp]],ed[a[tmp]],-1);//修改子树 } if(ch=='?') { cin>>s; int ans=query(s); cout<<ans<<endl; } } return 0; } ``` ~~人傻常数大~~ 再来一题:[CF1437G](https://www.luogu.com.cn/problem/CF1437G) 这题是~~我最喜欢的~~树剖 操作2换汤不换药,一眼 $fail$ 树上问题 树剖一下,线段树单点修维护区间最值搞掉 ~~等下答案是啥还没说~~ 咳咳...答案就是当前字符串在树内节点到根节点链上的最大值(自行思考为什么 然后是喜闻乐见的代码时间: ```cpp #include<bits/stdc++.h> #define ll int //依然抄板子 using namespace std; const int N=3e5+5,M=3e5+5; int n,m,id[M],top[M],fa[M],son[M],sz[M],at[N],cnt,tot,dep[M],val[N]; multiset<int>st[N];//可能多次出现,维护最大值 string s; vector<int>v[M]; struct node{ int vis[26]; int fail; int end; }AC[M]; inline void insert(string s,int idd) { int l=s.length(); int now=0; for(int i=0;i<l;i++) { if(AC[now].vis[s[i]-'a']==0) AC[now].vis[s[i]-'a']=++cnt; now=AC[now].vis[s[i]-'a']; } at[idd]=now+1; return; } inline void get_fail() { queue<int>q; for(int i=0;i<26;i++) { if(AC[0].vis[i]!=0) { AC[AC[0].vis[i]].fail=0; q.push(AC[0].vis[i]); } } while(!q.empty()) { int u=q.front(); q.pop(); for(int i=0;i<26;i++) { if(AC[u].vis[i]!=0) { AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i]; q.push(AC[u].vis[i]); } else AC[u].vis[i]=AC[AC[u].fail].vis[i]; } } return; } inline void dfs1(int p,int faa) { fa[p]=faa; dep[p]=dep[faa]+1; sz[p]=1; for(int t:v[p]) { if(t==faa)continue; dfs1(t,p); sz[p]+=sz[t]; if(sz[t]>sz[son[p]])son[p]=t; } return; } inline void dfs2(int p,int faa) { top[p]=faa; id[p]=++tot; if(!son[p])return; dfs2(son[p],faa); for(int t:v[p]) { if(t==fa[p]||t==son[p])continue; dfs2(t,t); } return; } struct nnode{ ll l,r,v; }; struct Tree{ nnode Tr[M<<2]; inline ll ls(ll id){return id<<1;} inline ll rs(ll id){return id<<1|1;} inline void pushup(ll id){Tr[id].v=max(Tr[ls(id)].v,Tr[rs(id)].v);return;} inline void build(ll id,ll l,ll r) { Tr[id]={l,r,-1}; if(l==r) { return; } ll mid=l+r>>1; build(ls(id),l,mid),build(rs(id),mid+1,r); pushup(id); return; } inline void modify(ll id,ll p,ll k) { ll l=Tr[id].l,r=Tr[id].r; if(l==r) { Tr[id].v=k; return; } ll mid=l+r>>1; if(p<=mid)modify(ls(id),p,k); if(p>mid)modify(rs(id),p,k); pushup(id); return; } inline ll query(ll id,ll l,ll r) { if(l<=Tr[id].l&&Tr[id].r<=r) { return Tr[id].v; } ll mid=Tr[id].l+Tr[id].r>>1,ans=-1; if(l<=mid)ans=max(ans,query(ls(id),l,r)); if(r>mid)ans=max(ans,query(rs(id),l,r)); return ans; } }T; inline int query(int x) { int res=-1;//如果找不到 while(x) { res=max(res,T.query(1,id[top[x]],id[x])); x=fa[top[x]]; } return res; } inline int query(string s) { int l=s.length(); int now=0,ans=-1;//如果找不到 for(int i=0;i<l;i++) { now=AC[now].vis[s[i]-'a']; ans=max(ans,query(now+1));//AC自动机下标从0开始,线段树不能,所以加1 } return ans; } int main() { // freopen(".in","r",stdin); // freopen(".out","w",stdout); ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>s; insert(s,i); } get_fail(); //建树 for(int i=1;i<=cnt;i++) v[AC[i].fail+1].push_back(i+1); dfs1(1,0); dfs2(1,1); T.build(1,1,tot); for(int i=1;i<=n;i++)T.modify(1,id[at[i]],0),st[at[i]].insert(0);//初始贡献为0,线段树内初值为-1 while(m--) { int op; cin>>op; if(op==1) { int i,x; cin>>i>>x; st[at[i]].erase(st[at[i]].lower_bound(val[i])); val[i]=x; st[at[i]].insert(x); T.modify(1,id[at[i]],*st[at[i]].rbegin()); } if(op==2) { cin>>s; cout<<query(s)<<endl; } } return 0; } ``` AC自动机告一段落,纸糊船掰掰ヾ(•ω•`)o 最后推个题单(我还没~~写~~学完 [AC自动机题单](https://www.luogu.com.cn/training/60465)