AC自动机
AC自动机
AC自动机差不多是在trie树上加了处理了kmp, 并且进行了路径压缩。
学习资料:https://oi-wiki.org/string/ac-automaton/
重要性质
- AC自动fail指的是当前字符串最长的在trie树中以前缀出现的后缀,这保证了AC自动机可以在多个串中同时匹配。
- fail树中从根到叶子的一条路径就是不断在前面拼接字符串的过程(类比parent树)
- 匹配过程中找的一直是最大匹配,同时该点到根的路径上的所有单词也都匹配一次
- 被一个串包含并且在\(i\)点结尾的所有串都在\(i\)到根的路径上
- 包含串\(i\)的所有串都在fail树上\(i\)的子树中所有点在trie树上的子树并中。
P5357 【模板】AC自动机(二次加强版)
顾名思义,就是模板题。
#include<bits/stdc++.h>
using namespace std;
int head[210000],siz[210000];
struct node
{
int v,nex;
}bian[2100000];
int cnt;
char s[2100000];
void add(int x,int y)
{
bian[++cnt].v=y;
bian[cnt].nex=head[x];
head[x]=cnt;
}
void dfs(int x)
{
for(int i=head[x];i;i=bian[i].nex)
{
int v=bian[i].v;
dfs(v);
siz[x]+=siz[v];
}
}
queue<int> q;
int n,tr[210000][26],fail[210000],tot=1,mat[210000];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s);
int u=1;
for(int j=0;s[j];j++)
{
int c=s[j]-'a';
if(!tr[u][c]) tr[u][c]=++tot;
u=tr[u][c];
}
mat[i]=u;
}
for(int i=0;i<26;i++) tr[0][i]=1;
q.push(1);
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];//类似于kmp
q.push(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];//路径压缩
}
}//就是bfs,入队前处理好fail指针,出队时处理tr边。
scanf("%s",s);
int u=1;
for(int i=0;s[i];i++)
{
u=tr[u][s[i]-'a'];//不需要fail指针
++siz[u];
}
for(int i=2;i<=tot;i++) add(fail[i],i);
dfs(1);
for(int i=1;i<=n;i++) printf("%d\n",siz[mat[i]]);
return 0;
}//一般trie树根节点都设为1而不是0,避免出现负数。
P3966 [TJOI2013]单词
AC自动机最最最最基本的操作,正如上面所说,一直匹配的是所有单词里的最大匹配。
1.将文本串在树上跑,走到一个点就加加。
2.在fail树上求子树和(树上差分)。
P3121 [USACO15FEB]Censoring G
模板题。利用了栈的技巧而已,甚至没用到fail树。(两个栈)
P2444 [POI2000]病毒
在AC自动机中找环即可。
vis[tr[p][i]]|=vis[fail[tr[p][i]]];
类似这种操作可以对每个点处理出到在该点匹配上的所有子串的信息。
大多数题都会用到,基本只要需要对每个点记额外信息就会用到。
P2414 [NOI2011] 阿狸的打字机
\(m\)次询问。每次问\(x\)在\(y\)中的出现次数。
思考\(fail\)树的意义。\(fail[x]=y\)代表着字符串\(x\)包含\(y\)。
容易发现,如果\(y\)在\(x\)中出现,那么\(end[y]\)一定在\(fail\)树中根到x的路径上。
所以在\(dfs\)原\(trie\)树上字符串\(x\)时
假设我们\(dfs\)到了第\(i\)位,我们可以通过树上差分在\(fail\)树上将\(i\)到根结点的值全部加一
此时如果位置\(p\)被加了一,那么说明以\(p\)结尾的所有字符串都在字符串\(x\)出现了一次,且以第\(i\)位结尾。
那么每次查询只要查询子树差分和即可,\(dfs\)序+BIT即可
下面时\(dfs\) \(trie\)树的过程
void dfs2(int x)
{
for(int i=0;i<you[x].size();i++)
{
int v=you[x][i];
int p=xx[v];
ans[v]=cha(to[p])-cha(fr[p]);//dfs序+BIT
}//查询
for(int i=0;i<26;i++)
{
if(!vis[x][i]) continue;//不能是路径压缩的点
adddd(id[tr[x][i]],1);
dfs2(tr[x][i]);
adddd(id[tr[x][i]],-1);//回溯
}
}
CF1207G Indie Album
先把所有串存下来建AC自动机,然后就是上一题了。
P4052 [JSOI2007]文本生成器
AC自动机上DP模板题。
AC自动机的DP一般都比较套路。
大部分都可以设\(f[i][j]\)为匹配到第\(i\)位,当前在\(j\)结点的方案数。
那么按长度顺序转移即可。
本题需要一个小容斥。
可读文本即至少一个单词的文本可以转换为总方案数减去一个单词都没有的数量
即经典的至少一个\(->\)一个都没有
\(vis\)数组依旧需要\(|=vis[fail[]]\)
f[1][0]=1;
for(int k=0;k<m;k++)
for(int i=1;i<=tot;i++)
for(int j=0;j<26;j++)
if(!vis[tr[i][j]])//没有单词
f[tr[i][j]][k+1]=(f[tr[i][j]][k+1]+f[i][k])%mod;
\(tips:\)AC自动机DP需要格外注意初值,不能转移的一定设成0或者极值。
P3311 [SDOI2014] 数数
依旧时是AC自动机上DP。
首先看到\(n<=10^{1201}\),想到数位dp。
那么依旧是处理出\(vis\)数组直接数位dp即可。
int dfs(int pos,int ac,bool limit,bool ling)
{
if(vis[ac]) return 0;
if(pos==len+1) return 1;
int gao=limit?a[pos]:9;
if(f[pos][ac]!=-1&&!limit&&!ling) return f[pos][ac];
int tmp=0;
for(int i=0;i<=gao;i++)
tmp=(tmp+dfs(pos+1,(ling&&(i==0))?ac:tr[ac][i],limit&&(i==gao),ling&&(i==0)))%mod;
if(!limit&&!ling) f[pos][ac]=tmp;
return tmp%mod;
}
需要注意的是本题前导零的处理,如果此时还是前导零状态,那么只\(pos++\) \(ac\)不动即可。
CF163E e-Government
题意:每次询问给定一模式串,求所有文本串匹配次数之和,文本串需要支持插入和删除操作。
思路:依旧是\(fail\)树的应用。
反过来想可以发现,对于一个字符串\(x\),所有包含他的字符串在\(fail\)中一定处于他的子树内。
所以可以转化为子树加,单点求和问题。
最简单的方法便是\(dfs\)序了。
if(opt=='+')
{
int x;
cin>>x;
if(vis[x]) continue;
vis[x]=1;
jia(kai[mat[x]],1); //dfs序子树加。
jia(jie[mat[x]]+1,-1);
}
else if(opt=='-')
{
int x;
cin>>x;
if(!vis[x]) continue;
vis[x]=0;
jia(kai[mat[x]],-1);
jia(jie[mat[x]]+1,1);
}
此处\(vis\)代表当前字符串是否在树中,避免重复添加或者重复删除。
CF1202E You Are Given Some Strings...
题意:给定字符串集S和以字符串T
询问\(\sum_{i=1}^{n}\sum_{j=1}^{n}f(T,s_i+s_j)\)
\(f(i,j)\)表示\(j\)在\(i\)中的出现次数。
我们发现最后计算答案时可以枚举分界点
所以考虑两个串分开计算。
考虑先建AC自动机用T跑一遍,统计每个位置第一个串结尾匹配数量。
然后再将\(S,T\)都\(reverse\),统计每个位置二个串开头匹配数量。
最后\(ans=\sum_{i=1}^{n}f1[i]*f2[i+1]\)
算\(f1,f2\)就是处理出每个点在\(fail\)树上到根结点的路径和即可。
while(!q.empty())
{
int p=q.front();
q.pop();
for(int i=0;i<26;i++)
{
if(tr[p][i])
{
fail[tr[p][i]]=tr[fail[p]][i];
w[tr[p][i]]+=w[fail[tr[p][i]]];//在这里可以简便处理
q.push(tr[p][i]);
}
else tr[p][i]=tr[fail[p]][i];
}
}
P5840 [COCI2015]Divljak
AC自动机毕业题。
题意:
两种操作:
1.往字符串集里添加字符串S
2.查询有多少字符串包含字符串T
乍一看是道水题,直接\(dfs\)序+树上差分不就完了吗,类似P2414 [NOI2011] 阿狸的打字机
写完发现样例过不去.........
思考问题出在哪里。
当我们对一个字符串S路径加的时候,每个位置都加了一次
所以实际查询的是出现次数之和。
所以我们要做到的求倒根结点的路径并。
那么就是树论技巧了。
我们先把所有位置按照\(dfs\)序。
第一个点到根结点差分,后面的每个点到和前一个点的\(lca\)差分
如图所示
红色为查询的点,带星的是差分完加了1的点。
scanf("%s",s+1);
cnt=strlen(s+1);
int u=1;
for(int i=1;i<=cnt;i++)
{
u=tr[u][s[i]-'a'];
xun[i].id=u;
xun[i].w=id[u];
}
sort(xun+1,xun+cnt+1,cmp);
for(int i=1;i<=cnt;i++)
{
jia(id[xun[i].id],1);
if(i!=1) jia(id[lca(xun[i].id,xun[i-1].id)],-1);
}
CF1327G Letters and Question Marks
一开始以为是'a'-'z'....后来发现是'a'-'n'.(草
因为能填充只有14个字母,所以考虑状压dp。
设\(f[i][j][S]\)为在序列第\(i\)位,ACAM上第\(j\)位,当前用过的字母集合为\(S\)。
复杂度为\(O(nm2^{14}*14)\)
复杂度爆炸。
观察到没有问号的连续段路径是确定的,所以可以通过问号将路径分段。
设\(g[i][j]\)为从\(i\)点出发,走第\(j\)段的价值,\(pos[i][j]\)为走完后的位置。
这样序列就会被分为最多15段。
但是暴力转移复杂度为\(O(14*m*2^{14}*14)\)
依旧无法接受。
但是我们发现没有必要枚举序列第几段,因为可以通过S计算出,有\(i\)个问号则当前是\(i+1\)段。
复杂度优化掉了一个14的常数,可以通过。
for(int S=0;S<(1<<14);S++)
{
int num=get(S)+1;
if(num>cnt) continue;
if(num==cnt)
{
for(int i=1;i<=tot;i++)
ans=max(ans,f[i][S]);
continue;
}
for(int i=1;i<=tot;i++)
for(int j=0;j<14;j++)
if(!((1<<j)&S))
{
int u=tr[i][j],v=pos[u][num];
f[v][S|(1<<j)]=max(f[v][S|(1<<j)],f[i][S]+g[u][num]+w[u]);
}
}

浙公网安备 33010602011771号