AC自动机
又是一个学了不知道多少遍也记不起来的东西。
从头再捋一遍吧。
AC自动机(简单版)
题意:
给定 \(n\) 个模式串 \(s_i\) 和一个文本串 \(t\),求有多少个不同的模式串在文本串里出现过。
两个模式串不同当且仅当他们编号不同。
处理多个字符串之间的关系,我们多用 \(trie\) 树这种数据结构。
对于本题,我们先对所有模式串建出 \(trie\) 树。
一个朴素的想法是枚举文本串的所有前缀,然后在 \(trie\) 树上查询所有是当前串后缀的串。
于是就在 \(trie\) 树上从头开始匹配。
但是匹配失败了的话怎么办?
直接暴力再重新找是无法接受的。
如果我们可以快速地从不合法位置跳到下一个合法位置,那就好办了。
下一个合法位置便是当前匹配到位置的最长后缀,这个东西是可以在 \(trie\) 树上预处理出来的。
一般把这玩意叫做失配指针,也就是 \(fail\) 指针。
怎么预处理出这个东西呢?
首先在 \(trie\) 树上 \(bfs\),这样就能保证自己的 \(fail\) 在自己之前更新过了。
然后有一个关键的式子:
\(fail[tr[u][c]]=tr[fail[u]][c]\)
如果 \(fail[u]\) 存在 \(c\) 这个儿子的话,这当然是对的。
如果不存在呢?
那我们考虑提前在 \(fail[u]\) 处处理这个问题。
在我们 \(bfs\) 到 \(fail[u]\) 这个点时,我们枚举它的所有儿子。
如果有一个儿子不存在,就令 \(tr[u][c]=tr[fail[u]][c]\)。
这样就完整地保存了上一个有用的 \(fail\) 信息了。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,fail[N*30],tr[N][30],vis[N],viss[N],tot,ans;
string s;
void insert(){
int u=0;
for(int i=0;s[i];i++){
int c=s[i]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
vis[u]++;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]) fail[tr[u][i]]=tr[fail[u]][i],q.push(tr[u][i]);
else tr[u][i]=tr[fail[u]][i];
}
}
}
void query(){
int u=0;
for(int i=0;s[i];i++){
int c=s[i]-'a';
u=tr[u][c];
int j=u;
while(j&&!viss[j]){
if(viss[j]) break;
if(vis[j]){
ans+=vis[j];
}
viss[j]=1;
j=fail[j];
}
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>s;
insert();
}
build();
cin>>s;
query();
cout<<ans<<'\n';
}
AC自动机(简单版II)
题意:求出所有在文本串中出现次数最多的模式串。
和上一个一样,先建出 AC 自动机,然后考虑怎么查询。
一个朴素的想法是一直跳 \(fail\) 直到跳到根节点,然后贡献到经历过的节点上。
这样就可以通过本题了。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N],t,n,tr[N][30],tot,res[N],fail[N],vis[N];
string s[N],tt;
void insert(int id){
int u=0;
for(int i=0;s[id][i];i++){
int c=s[id][i]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
a[u]=id;
vis[u]=1;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q.push(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
void query(){
int u=0;
for(int i=0;tt[i];i++){
int c=tt[i]-'a';
u=tr[u][c];
int j=u;
while(j){
if(vis[j]) res[a[j]]++;
j=fail[j];
}
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
while(1){
tot=0;
memset(tr,0,sizeof tr);
memset(fail,0,sizeof fail);
memset(vis,0,sizeof vis);
memset(res,0,sizeof res);
cin>>n;
if(!n) break;
for(int i=1;i<=n;i++){
cin>>s[i];
insert(i);
}
build();
cin>>tt;
query();
int k=0;
for(int i=1;i<=n;i++){
k=max(k,res[i]);
}
cout<<k<<'\n';
for(int i=1;i<=n;i++){
if(res[i]==k) cout<<s[i]<<'\n';
}
}
}
但是太暴力了,我不满意。
假设我们将所有位置向它们的 \(fail\) 连边,可以发现会形成一棵树。
那么每次相当于从当前点到根上这条链加1。
这是好做的,每次将贡献挂到当前点上,最后再 \(dfs\) 一遍算出子树和就可以得到答案。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N],n,tr[N][30],tot,siz[N],fail[N],vis[N];
string s[N],tt;
vector<int> e[N];
void insert(int id){
int u=0;
for(int i=0;s[id][i];i++){
int c=s[id][i]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
a[u]=id;
vis[u]=1;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++){
if(tr[0][i]){
q.push(tr[0][i]);
e[0].push_back(tr[0][i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q.push(tr[u][i]);
e[fail[tr[u][i]]].push_back(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
void query(){
int u=0;
for(int i=0;tt[i];i++){
int c=tt[i]-'a';
u=tr[u][c];
siz[u]++;
}
}
void dfs(int u){
for(int i=0;i<(int)e[u].size();i++){
int v=e[u][i];
dfs(v);
siz[u]+=siz[v];
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
while(1){
tot=0;
memset(tr,0,sizeof tr);
memset(fail,0,sizeof fail);
memset(vis,0,sizeof vis);
memset(siz,0,sizeof siz);
cin>>n;
if(!n) break;
for(int i=1;i<=n;i++){
cin>>s[i];
insert(i);
}
build();
cin>>tt;
query();
dfs(0);
int k=0;
for(int i=1;i<=tot;i++){
if(vis[i]) k=max(k,siz[i]);
}
cout<<k<<'\n';
for(int i=1;i<=tot;i++){
if(siz[i]==k&&vis[i]) cout<<s[a[i]]<<'\n';
}
for(int i=0;i<=tot;i++) e[i].clear();
}
}
[模板]AC自动机
题意:求每个模式串在文本串中出现的次数。
和上一个题差不多,注意模式串可能重复。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,tr[N][30],tot,siz[N],fail[N],vis[N],cnt[N];
string s[N],tt;
vector<int> e[N],a[N];
void insert(int id){
int u=0;
for(int i=0;s[id][i];i++){
int c=s[id][i]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
a[u].push_back(id);
vis[u]=1;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++){
if(tr[0][i]){
q.push(tr[0][i]);
e[0].push_back(tr[0][i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q.push(tr[u][i]);
e[fail[tr[u][i]]].push_back(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
void query(){
int u=0;
for(int i=0;tt[i];i++){
int c=tt[i]-'a';
u=tr[u][c];
siz[u]++;
}
}
void dfs(int u){
for(int i=0;i<(int)e[u].size();i++){
int v=e[u][i];
dfs(v);
siz[u]+=siz[v];
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>s[i];
insert(i);
}
build();
cin>>tt;
query();
dfs(0);
for(int i=1;i<=tot;i++){
if(vis[i]){
for(auto v:a[i]) cnt[v]=siz[i];
}
}
for(int i=1;i<=n;i++) cout<<cnt[i]<<'\n';
}
L语言
设 \(f_i\) 表示前缀 \(1\sim i\) 能否被理解。
建出 AC 自动机后拿文本串在上面匹配,记录每个节点的深度。
暴力跳 \(fail\) 就可以直接转移。
但是过不了,复杂度不对。
发现 \(trie\) 树的深度 \(<20\),所以可以状压。
改变一下 \(f_i\) 的定义:在 \(fail\) 树上,从 \(i\) 到根的所有合法单词。
然后就可以直接判断是否合法了。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+10;
int n,m,tr[N][30],tot,vis[N*30],len,dep[N*30],fail[N*30],f[N];
string s;
void insert(){
int u=0;
for(int i=0;s[i];i++){
int c=s[i]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
f[u]|=1<<(s.size()-1);
}
void build(){
queue<int> q;
for(int i=0;i<26;i++){
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()){
int u=q.front();
q.pop();
f[u]|=f[fail[u]];
for(int i=0;i<26;i++){
if(tr[u][i]) fail[tr[u][i]]=tr[fail[u]][i],q.push(tr[u][i]);
else tr[u][i]=tr[fail[u]][i];
}
}
}
void query(){
int u=0,now=1,ans=0;
for(int i=0;s[i];i++){
int c=s[i]-'a';
u=tr[u][c];
if(now&f[u]) now=now<<1|1,ans=i+1;
else now=now<<1;
}
cout<<ans<<'\n';
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s;
insert();
}
build();
for(int i=1;i<=m;i++){
cin>>s;
query();
}
return 0;
}
[JSOI2007] 文本生成器
题意:给定 \(n\) 个字符串,求长度为 \(m\) 的至少存在一个子串是 \(n\) 个字符串之一的字符串的个数。
反着做,用总方案数减去没有子串是 \(n\) 个字符串之一的长度为 \(m\) 的串的个数。
总方案数就是 \(26^m\)。
剩下部分 \(dp\) 来做:
设 \(f_{i,j}\) 表示长度为 \(i\) 的在 \(trie\) 图上走完之后到了 \(j\) 的合法字符串个数。
\(trie\) 图就是建完 \(fail\) 树后的 \(trie\) 树,但是由于多建了空节点向 \(fail\) 的连边所以成了 \(trie\) 图。
每个字符串都是由前缀转移而来,所以限制就是后缀中不出现那 \(n\) 个字符串。
在建 \(fail\) 时处理一下,每个点考虑一下其 \(fail\) 是否被限制即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+10,mod=1e4+7;
int n,m,tr[N][30],vis[N],tot,fail[N],f[110][N];
string s;
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
void insert(){
int u=0;
for(int i=0;s[i];i++){
int c=s[i]-'A';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
vis[u]=1;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
vis[tr[u][i]]|=vis[fail[tr[u][i]]];
q.push(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s;
insert();
}
build();
f[0][0]=1;
for(int i=1;i<=m;i++){
for(int j=0;j<=tot;j++){
for(int k=0;k<26;k++){
int v=tr[j][k];
if(!vis[v]) (f[i][v]+=f[i-1][j])%=mod;
}
}
}
int ans=ksm(26,m);
for(int i=0;i<=tot;i++){
ans=(ans-f[m][i]+mod)%mod;
}
cout<<ans<<'\n';
}
Divljak
披着 AC 自动机外壳的树论问题。
首先对 S 建出 AC 自动机,同时建出 \(fail\) 树。
每次插入一个串,相当于拿它在 \(trie\) 图上走,走到节点到根的链的并 \(+1\)。
树上差分一下,这个东西可以这么实现:
将走到的点拿出来,按 \(dfn\) 排序。
先将所有点位置处 \(+1\), 再将相邻点 \(LCA\) 处 \(-1\)。
查询时直接查子树和即可。
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+10;
int n,q,tr[N][30],tot,mxs[N],siz[N],dep[N],topf[N],fail[N],dfn[N],idx,C[N],pt[N],nrd[N];
vector<int> e[N];
string s;
int lowbit(int x){
return x&-x;
}
void add(int x,int k){
while(x<=tot+1){
C[x]+=k;
x+=lowbit(x);
}
}
int query(int x){
int res=0;
while(x){
res+=C[x];
x-=lowbit(x);
}
return res;
}
void insert(int id){
int u=0;
for(int i=0;s[i];i++){
int c=s[i]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
pt[id]=u;
}
void build(){
queue<int> q;
for(int i=0;i<26;i++){
if(tr[0][i]){
q.push(tr[0][i]);
e[0].push_back(tr[0][i]);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q.push(tr[u][i]);
e[fail[tr[u][i]]].push_back(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
void dfs1(int u,int fa){
if(u) dep[u]=dep[fa]+1;
siz[u]=1;
for(int i=0;i<(int)e[u].size();i++){
int v=e[u][i];
if(v==fa) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(!mxs[u]||siz[v]>siz[mxs[u]]) mxs[u]=v;
}
}
void dfs2(int u,int tp){
dfn[u]=++idx;
nrd[idx]=u;
topf[u]=tp;
if(mxs[u]) dfs2(mxs[u],tp);
for(int i=0;i<(int)e[u].size();i++){
int v=e[u][i];
if(v==fail[u]||v==mxs[u]) continue;
dfs2(v,v);
}
}
int lca(int x,int y){
if(dep[topf[x]]<dep[topf[y]]) swap(x,y);
while(topf[x]!=topf[y]){
x=fail[topf[x]];
if(dep[topf[x]]<dep[topf[y]]) swap(x,y);
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
void update(){
int u=0;
set<int> k;
for(int i=0;s[i];i++){
int c=s[i]-'a';
u=tr[u][c];
k.insert(dfn[u]);
}
for(auto i=k.begin();i!=k.end();i++){
add(*i,1);
}
for(auto i=++k.begin();i!=k.end();i++){
auto j=i;
int x=nrd[*(--j)],y=nrd[*(++j)];
add(dfn[lca(x,y)],-1);
}
}
int main(){
#ifndef ONLINE_JUDGE
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>s;
insert(i);
}
build();
dep[0]=1;
dfs1(0,-1);
dfs2(0,0);
cin>>q;
while(q--){
int op,x;
cin>>op;
if(op==1){
cin>>s;
update();
}
else{
cin>>x;
x=pt[x];
cout<<query(dfn[x]+siz[x]-1)-query(dfn[x]-1)<<'\n';
}
}
}
本文作者:Hugoi
本文链接:https://www.cnblogs.com/hugoi/p/18533968
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】