AC自动机学习笔记
AAA404
·
·
个人记录
基础的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)