AC自动机总结
AC自动机简述
功能
多模板串对单个或多个串的匹配问题
主体思想
原理同\(kmp\) , 在\(trie\)树上使用变种的\(kmp\)
实现
需要数组 : \(trie[N][26],fail[N]\)
\(fail\)即我们所说的失配函数,\(trie[]\)则略有变更
准确一点得说,\(fail\)函数是不需要知道后继字母的失配
而\(trie\)树上的节点经过处理后,就可以直接\(O(1)\)访问后继每一种字母的情况的下一个位置,不需要再一次次失配
预处理
AC自动机的预处理与\(kmp\)的预处理只有一点不同
\(kmp\)的 失配数组\(nxt[N]\) , 是在不知道下一位的字母,一次次失配直到与下一位匹配
int j=0;
for(int i=1;i<=n;++i){
while(j && s[i]!=t[j+1]) j=nxt[j];
if(s[i]==t[j+1]) j++;
}
然而AC自动机既然已经基于\(trie\)树结构,自然可以对于每个下一位字母的情况来匹配,这里我们分类讨论
如果已经存在同字母的节点,那么就是下一位节点,而它的\(fail\)就是上一个失配位置\(fail\)的这一个字母的后继节点
如果不存在,就是上一个失配位置\(fail\)的这一个字母的后继节点
(?Are You Kidding Me ?)
事实上是,AC自动机上的\(trie\)树节点,对于不存在的点,我们开出一个虚点作为这个点下一位是这个字母的nxt
而这个过程就可以通过上面分类讨论里描述的方式递推
这样处理下一位的匹配时,就能够直接访问\(trie\)树上所指向的节点
预处理我们通过广搜来递推每个点的\(fail,trie[]\)
struct AC_automation{
static const int SIZE=N*26;
int trie[SIZE][26],cnt,fail[SIZE];
void Build(char s[N][51]){
rep(i,1,n) Insert(s[i]);
static queue que;
rep(i,0,25) if(trie[0][i]) {
que.push(trie[0][i]);
fail[trie[0][i]]=0;
}
while(!que.empty()) {
int u=que.front(); que.pop();
rep(i,0,25) {
int &v=trie[u][i];
if(v) {
fail[v]=trie[fail[u]][i];
que.push(v);
} else v=trie[fail[u]][i];
}
}
}
}AC;
如果你还不清晰,那没有关系,大不了我们先从背板子做起
关于插叙操作的实现
我们已经知道,每个点的后继状态可以直接通过\(trie\)数组访问
而这里的查询并不只有这么简单
事实上,每一个节点对应的不止是这个节点上所对应的的模版串末尾,
因为模版串直接会含有前后缀的包含关系,所以我们这里要通过一次次的强行失配来访问这个节点对应的所有后缀包含的模版串
写成代码就是
void Que(char *s){
int p=0,n=strlen(s+1);
rep(i,1,n) {
p=trie[p][s[i]-'a'];
for(int j=p;j;j=fail[j]) {
;
;//这里该干啥干啥
}
}
}
对于最基础的单串访问,我们直接开一个标记记录这个节点是否被加过答案即可
struct AC_automation{
static const int SIZE=N*26;
int trie[SIZE][26],End[SIZE],cnt,fail[SIZE],vis[SIZE];
int Query(char *s) {
memset(vis,0,sizeof vis);
int Ans=0;
int p=0;
rep(i,0,strlen(s)-1) {
int x=s[i]-'a';
p=trie[p][x];
for(int j=p;j && !vis[j];j=fail[j]) {//注意如果已经访问就可以break了
Ans+=End[j];
vis[j]=1;
}
}
return Ans;
}
}AC;
int main(){
rep(kase,1,rd()) {
AC.clear();
n=rd();
rep(i,1,n) scanf("%s",s[i]);
AC.Build(s);
scanf("%s",str);
printf("%d\n",AC.Query(str));
}
}
好的现在我们可以愉快得去A掉模板题了
话说我还写了一种指针的,由于是第一次写指针,写的比较粗糙,跑得也不快
int n,m,k;
char s[N][51];
char str[M];
struct AC_automation{
struct Node{
int End,vis;
Node *son[26],*fail;
Node(){
End=0;
rep(i,0,25) son[i]=NULL;
fail=NULL;
vis=0;
}
} rt;
void clear(){ rt=Node(); }
void Insert(char *s){
Node *p=&rt;
rep(i,0,strlen(s)-1) {
int x=s[i]-'a';
if(p->son[x]==NULL) p->son[x]=new Node();
p=p->son[x];
}
p->End++;
}
void Build(char s[N][51]){
rep(i,1,n) Insert(s[i]);
static queue que;
rep(i,0,25) if(rt.son[i]!=NULL) {
que.push(rt.son[i]);
rt.son[i]->fail=&rt;
} else rt.son[i]=&rt;
while(!que.empty()) {
Node *u=que.front(); que.pop();
rep(i,0,25) {
if(u->son[i]!=NULL) {
u->son[i]->fail=u->fail->son[i];
que.push(u->son[i]);
} else u->son[i]=u->fail->son[i];
}
}
}
int Query(char *s) {
int Ans=0;
Node *p=&rt;
rep(i,0,strlen(s)-1) {
int x=s[i]-'a';
p=p->son[x];
for(Node *j=p;j!=NULL && !j->vis; j=j->fail) {
Ans+=j->End;
j->vis=1;
}
}
return Ans;
}
}AC;
int main(){
rep(kase,1,rd()) {
AC.clear();
n=rd();
rep(i,1,n) scanf("%s",s[i]);
AC.Build(s);
scanf("%s",str);
printf("%d\n",AC.Query(str));
}
}
//http://acm.hdu.edu.cn/showproblem.php?pid=2222
学完模板的旁友们先不要跑!你们还不会多串匹配呢!
观察到我们的每个点都有一个所指向的\(fail\)节点,令这个点为父亲,我们就能得到一棵有\(fail\)指针构成的树,暂且称其为\(fail\)树吧
所以我们访问每个节点时,其实就是访问到了其在\(fail\)树上所对应的的一段到根的路径上所对应的点
这个东西我们就可以有请各种神仙解法来维护啦(建议自己think think)
练习
温馨提示:AC自动机的练习题可能卡内存
POJ - 1204
把要查询的串都扔进AC自动机,然后暴力check就是了
const int z[10][4]={{-1,0},{-1,1},{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1}};
int n,m,q;
char s[N][N];
char str[N][N];
int px[N],py[N],dir[N];
namespace AC{
static const int SIZE=1e6+10;
int trie[SIZE][26];
vector End[SIZE];
int fail[SIZE],cnt,vis[SIZE];
int Insert(char *s){
int p=0;
rep(i,0,strlen(s)-1) {
int x=s[i]-'A';
((!trie[p][x])&&(trie[p][x]=++cnt));
p=trie[p][x];
}
//cout<<"Add"< que;
rep(i,0,25) if(trie[0][i]) que.push(trie[0][i]);
while(!que.empty()) {
int u=que.front(); que.pop();
//cout<=n||y>=m||x<0||y<0) return;
dfs(x,y,d,p);
}
}
using AC::dfs;
int main(){
n=rd(),m=rd(),q=rd();
rep(i,0,n-1) scanf("%s",s[i]);
rep(i,1,q) scanf("%s",str[i]);
AC::Build(str);
rep(i,0,m-1) dfs(n-1,i,0,0);
rep(i,0,n-1) dfs(i,0,1,0); rep(j,0,m-1) dfs(n-1,j,1,0);
rep(i,0,n-1) dfs(i,0,2,0);
rep(i,0,n-1) dfs(i,0,3,0); rep(j,0,m-1) dfs(0,j,3,0);
rep(i,0,m-1) dfs(0,i,4,0);
rep(i,0,m-1) dfs(0,i,5,0); rep(i,0,n-1) dfs(i,m-1,5,0);
rep(i,0,n-1) dfs(i,m-1,6,0);
rep(i,0,n-1) dfs(i,m-1,7,0); rep(j,0,m-1) dfs(n-1,j,7,0);
rep(i,1,q) {
int l=strlen(str[i])-1;
printf("%d %d %c\n",px[i]-l*z[dir[i]][0],py[i]-l*z[dir[i]][1],'A'+dir[i]);
}
}
ZOJ - 3228
我写的比较奇怪
先把模板串都丢进去
然后跑插叙
对于允许重叠的,我们直接对\(fail\)树上一段路径的节点的答案++
否则我们分串的长度讨论,对于每种长度的串处理一个答案\(dp[i][6]\)
int n;
char s[N][10],str[N];
int kind[N],End[N];
const int SIZE=N*6;
int dp[SIZE][7],pre[SIZE][7];
namespace AC{
int trie[SIZE][26];
int fail[SIZE],cnt;
void clear(){
cnt=0;
memset(trie,0,sizeof trie);
}
int Insert(char *s){
int p=0;
rep(i,0,strlen(s)-1) {
int x=s[i]-'a';
((!trie[p][x])&&(trie[p][x]=++cnt));
p=trie[p][x];
}
return p;
}
void Build(){
static queue que;
rep(i,0,25) if(trie[0][i]) {
que.push(trie[0][i]);
fail[trie[0][i]]=0;
}
while(!que.empty()) {
int u=que.front(); que.pop();
rep(i,0,25) {
int &v=trie[u][i];
if(v) {
fail[v]=trie[fail[u]][i];
que.push(v);
} else v=trie[fail[u]][i];
}
}
}
void Que(char *s) {
memset(dp,0,sizeof dp);
memset(pre,-63,sizeof pre);
int p=0;
rep(i,0,strlen(s)-1) {
p=trie[p][s[i]-'a'];
for(reg int j=p; j; j=fail[j]) {
dp[j][0]++;
rep(k,1,6) {
if(i-pre[j][k]>=k) {
dp[j][k]++;
pre[j][k]=i;
}
}
}
}
}
}
int main(){
int kase=0;
while(~scanf("%s",str)) {
printf("Case %d\n",++kase);
AC::clear();
n=rd();
rep(i,1,n) {
kind[i]=rd();
scanf("%s",s[i]);
if(kind[i]) kind[i]=strlen(s[i]);
End[i]=AC::Insert(s[i]);
}
AC::Build();
AC::Que(str);
rep(i,1,n) printf("%d\n",dp[End[i]][kind[i]]);
puts("");
}
}
\[ \ \]
\[ \ \]
HDU - 2457
把AC自动机上的状态存进dp状态里即可
const int N=1e3+10,M=1e7+100,INF=1e9+10;
const int z[10][4]={{-1,0},{-1,1},{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1}};
void chk(int &a,int b){ ((a>b)&&(a=b)); }
int n;
char s[N];
int ch[N];
//AC automation
const int SIZE=N;
int trie[SIZE][4];
int End[SIZE];
int fail[SIZE],cnt;
void clear(){
cnt=0;
memset(End,0,sizeof End);
memset(trie,0,sizeof trie);
memset(fail,0,sizeof fail);
}
void Insert(char *s){
int p=0;
rep(i,0,strlen(s)-1) {
int x=ch[(int)s[i]];
if(!trie[p][x]) trie[p][x]=++cnt;
p=trie[p][x];
}
End[p]=1;
}
void Build() {
static queue que;
rep(i,0,3) if(trie[0][i]) que.push(trie[0][i]);
while(!que.empty()) {
int u=que.front(); que.pop();
End[u]|=End[fail[u]];
rep(i,0,3) {
int &v=trie[u][i];
if(v) {
que.push(v);
fail[v]=trie[fail[u]][i];
} else v=trie[fail[u]][i];
}
}
}
int dp[N][N];
int kase;
int main(){
ch[(int)'A']=0,ch[(int)'T']=1,ch[(int)'C']=2,ch[(int)'G']=3;
while(~scanf("%d",&n) && n) {
clear();
rep(i,1,n) scanf("%s",s),Insert(s);
Build();
scanf("%s",s+1);
int m=strlen(s+1);
memset(dp,63,sizeof dp);
dp[0][0]=0;
rep(i,1,m) {
rep(j,0,cnt) if(!End[j] && dp[i-1][j]
\[ \ \]
\[ \ \]
POJ - 2778
再套一个矩阵就好了
int n,m;
int a[N];
char s[N];
int val[N];
//AC automation
const int SIZE=101;
int trie[SIZE][4];
int End[SIZE];
int fail[SIZE],cnt;
int ch[N];
int Insert(char *s){
int p=0;
int l=0;
while(s[l]!='\0') l++;
rep(i,0,l-1) {
int x=ch[(int)s[i]];
//cout<<"insert"< que;
rep(i,0,3) if(trie[0][i]) que.push(trie[0][i]);
while(!que.empty()) {
int u=que.front(); que.pop();
End[u]|=End[fail[u]];
rep(i,0,3) {
int &v=trie[u][i];
if(v) {
que.push(v);
fail[v]=trie[fail[u]][i];
} else v=trie[fail[u]][i];
}
}
}
struct Mat{
int a[SIZE][SIZE];
void init(){ memset(a,0,sizeof a); }
void Get1(){ rep(i,0,cnt) a[i][i]=1; }
Mat operator * (const Mat x) const{
Mat res; res.init();
rep(i,0,cnt) rep(j,0,cnt) rep(o,0,cnt) res.a[i][o]=(res.a[i][o]+1ll*a[i][j]*x.a[j][o])%P;
return res;
}
}x,res;
int f[1][SIZE],ans[1][SIZE];
int main(){
ch[(int)'A']=0,ch[(int)'T']=1,ch[(int)'C']=2,ch[(int)'G']=3;
m=rd(),n=rd();
rep(i,1,m) {
scanf("%s",s);
End[Insert(s)]=1;
}
Build();
//puts("!");
f[0][0]=1;
rep(i,0,cnt) if(!End[i]) {
rep(j,0,3) {
int nxt=trie[i][j];
if(End[nxt]) continue;
x.a[i][nxt]++;
}
}
res.Get1();
//puts("!");
while(n) {
if(n&1) res=res*x;
x=x*x;
n>>=1;
}
rep(i,0,0) rep(j,0,cnt) rep(o,0,cnt) ans[i][o]=(ans[i][o]+1ll*f[i][j]*res.a[j][o])%P;
int Ans=0;
rep(i,0,cnt) (Ans+=ans[0][i])%=P;
printf("%d\n",Ans);
}
\[ \ \]
\[ \ \]
HYSBZ - 3172
嗯,题目中的文章是由所有单词拼出来的,但是单词直接互相独立
这道题我们才第一次用到一点\(fail\)树
首先单词都丢进AC自动机,然后一个个跑匹配
每一次更新\(fail\)树上一段路径前缀的节点即可
事实上就是在哪里放一个1,按次向根累加就能得到答案
int n;
int l;
string s[N];
int End[N];
const int SIZE=N;
int trie[N][26],fail[N],cnt;
int Insert(string &s){
int p=0;
rep(i,0,s.size()-1) {
int x=s[i]-'a';
if(!trie[p][x]) trie[p][x]=++cnt;
p=trie[p][x];
}
return p;
}
int line[SIZE],Ans[SIZE],lc;
void Build(){
static queue que;
rep(i,0,25) if(trie[0][i]) que.push(trie[0][i]);
while(!que.empty()){
int u=que.front(); que.pop();
line[++lc]=u;
rep(i,0,25) {
int &v=trie[u][i];
if(v) {
que.push(v);
fail[v]=trie[fail[u]][i];
} else v=trie[fail[u]][i];
}
}
}
int main(){
ios::sync_with_stdio(false);
cin>>n;
rep(i,1,n) {
cin>>s[i];
End[i]=Insert(s[i]);
}
Build();
rep(i,1,n){
int p=0;
rep(j,0,s[i].size()-1) {
p=trie[p][s[i][j]-'a'];
Ans[p]++;
}
}
drep(i,lc,1) Ans[fail[line[i]]]+=Ans[line[i]];//利用广搜序累和
rep(i,1,n) printf("%d\n",Ans[End[i]]);
}
\[ \ \]
\[ \ \]
HDU - 5069
求后缀与前缀的最大匹配长度
这题我写的很暴力
首先对于前一个字符串,直接匹配到末尾,其实接下来就是求\(fail\)树上的这段前缀上的每一个点与后一个串在\(trie\)树上的位置的最长公共前缀长度
我的做法是:将\(trie\)树树剖,依次访问\(fail\)树上的每一个节点回答询问
访问\(fail\)树上的节点前缀时就可以直接通过每经过一个点就++
对于这一段前缀的一个询问串x,令其在\(trie\)树上的末尾节点为y,我们就是找到所有++的节点最深的在\(y\)对应的\(trie\)树前缀上的位置,这里我直接树剖加二分实现了
int n,m;
string s[N];
const int SIZE=N;
int End[N];
int trie[N][26],fail[N],cnt;
void clear(){
memset(trie,0,sizeof trie);
cnt=0;
}
int Insert(string &s){
int p=0;
rep(i,0,s.size()-1) {
int x=s[i]-'A';
if(!trie[p][x]) trie[p][x]=++cnt;
p=trie[p][x];
}
return p;
}
struct Graph{
struct Edge{
int to,nxt;
}e[SIZE];
int head[N],ecnt;
void AddEdge(int u,int v){
//cout<<"Edge ont the fail "<sz[son[u]]) son[u]=v;
}
}
int L[N],R[N],dfn,id[N];
void dfs2(int u,int t){
//cout<<"dfs on the trie"< que;
dfn=0;G.clear();
fa[0]=-1,dfs1(0);dfs2(0,0);
rep(i,0,25) if(trie[0][i]) {
que.push(trie[0][i]);
fail[trie[0][i]]=0;
}
while(!que.empty()){
int u=que.front(); que.pop();
//cout<<"#"< > Q[N];
int sum[N];
void Add(int p,int x){
//cout<<"Add "< > tmp;
swap(tmp,Q[u]);
for(int i=G.head[u];i;i=G.e[i].nxt) {
int v=G.e[i].to;
dfs_getans(v);
}
Add(L[u],-1);
}
bool ed;
int main(){
ios::sync_with_stdio(false);
while(cin>>n>>m) {
clear();
rep(i,1,n) {
cin>>s[i];
End[i]=Insert(s[i]);
}
Build();
rep(i,1,m) {
int x=rd(),y=rd();
x=End[x],y=End[y];
Q[x].push_back(make_pair(y,i));
}
dfs_getans(0);
rep(i,1,m) printf("%d\n",Ans[i]);
}
}
\[ \ \]
\[ \ \]
HDU - 4117
这个就是dp从每一个的子串上转移过来
转移过程中不断匹配,然后统计\(fail\)树上前缀的最大值,这时一个动态的过程
于是我们树剖线段树
const int N=2e4+10,SIZE=2.3e5+10;
bool be;
int n,m;
string s[N];
int End[N],val[N];
int trie[SIZE][26],fail[SIZE],cnt;
void clear(){
memset(trie,0,sizeof (int) * (cnt+1)*26);
cnt=0;
}
int Insert(string &s){
int p=0;
rep(i,0,s.size()-1) {
//if(!isalpha(s[i])) while(1);
int x=s[i]-'a';
if(!trie[p][x]) trie[p][x]=++cnt;
p=trie[p][x];
}
return p;
}
int sz[SIZE],son[SIZE],top[SIZE],fa[SIZE];
struct Edge{
int to,nxt;
}e[SIZE];
int head[SIZE],ecnt;
void AddEdge(int u,int v){
e[++ecnt].to=v;
e[ecnt].nxt=head[u];
head[u]=ecnt;
}
void dfs1(int u){
sz[u]=1,son[u]=-1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!v) continue;
fa[v]=u;
dfs1(v);
sz[u]+=sz[v];
if(son[u]==-1||sz[v]>sz[son[u]]) son[u]=v;
}
}
int L[SIZE],R[SIZE],dfn;
//id[SIZE];
void dfs2(int u,int t){
top[u]=t;
L[u]=++dfn;
if(~son[u]) dfs2(son[u],t);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!v||v==son[u]) continue;
dfs2(v,v);
}
R[u]=dfn;
}
void chk(int &a,int b){ ((a>1;
if(x<=mid) Upd(p<<1,l,mid,x,y);
else Upd(p<<1|1,mid+1,r,x,y);
}
int Que(int p,int l,int r,int ql,int qr){
if(l==ql&&r==qr) return sum[p];
int mid=(l+r)>>1;
if(qr<=mid) return Que(p<<1,l,mid,ql,qr);
else if(ql>mid) return Que(p<<1|1,mid+1,r,ql,qr);
else return max(Que(p<<1,l,mid,ql,mid),Que(p<<1|1,mid+1,r,mid+1,qr));
}
}tr;
void Build(){
static queue que;
dfn=0;
memset(head,0,sizeof head);ecnt=0;
rep(i,0,25) if(trie[0][i]) {
que.push(trie[0][i]);
fail[trie[0][i]]=0;
}
while(!que.empty()){
int u=que.front(); que.pop();
AddEdge(fail[u],u);
rep(i,0,25) {
int &v=trie[u][i];
if(v) {
que.push(v);
fail[v]=trie[fail[u]][i];
} else v=trie[fail[u]][i];
}
}
fa[0]=-1,dfs1(0);dfs2(0,0);
}
bool ed;
int main(){
//cout<<&ed-&be<>T;
rep(kase,1,T) {
clear();
tr.clear();
cin>>n;
rep(i,1,n) {
cin>>s[i]>>val[i];
End[i]=Insert(s[i]);
}
Build();
int ans=0;
rep(i,1,n) {
int res=0,p=0;
rep(j,0,s[i].size()-1) {
p=trie[p][s[i][j]-'a'];
int x=p;
while(~x) {
chk(res,tr.Que(1,1,dfn,L[top[x]],L[x]));
x=fa[top[x]];
}
}
chk(res,res+val[i]);
tr.Upd(1,1,dfn,L[End[i]],res);
chk(ans,res);
string t;swap(t,s[i]);
}
printf("Case #%d: %d\n",kase,ans);
}
}
HDU - 6096
为什么我觉得这个题是最难的
我不晓得官方做法,于是参考一笑网上神仙的思想
把要匹配的串换过来中间加上一个奇怪的字符再相连
原字符串也复制一份,中间加上一个奇怪的字符,然后就可以直接求匹配了!
由于不能重复,所以会受到长度的限制,用树状数组统计
const int N=1e5+10,K=20,SIZE=1.5e6;
const char d='`';
int n,m;
string s[N],a,b;
int trie[SIZE][27],cnt,len[N],fail[SIZE];
vector vec[SIZE],Q[SIZE];
int End[N];
struct Edge{
int to,nxt;
}e[SIZE];
int head[SIZE],ecnt;
void AddEdge(int u,int v){
e[++ecnt]=(Edge){v,head[u]};
head[u]=ecnt;
}
void clear(){
memset(trie,0,sizeof (int) * (cnt+1) *27 );
memset(head,0,sizeof (int) * (cnt+1));
rep(i,0,cnt) vec[i].clear(),Q[i].clear();
cnt=0;
}
int Insert(string &s){
int p=0;
rep(i,0,s.size()-1) {
int x=s[i]-d;
if(!trie[p][x]) trie[p][x]=++cnt;
p=trie[p][x];
}
return p;
}
void Build(){
static queue que;
rep(i,0,26) if(trie[0][i]) {
que.push(trie[0][i]);
fail[trie[0][i]]=0;
}
while(!que.empty()) {
int u=que.front(); que.pop();
AddEdge(fail[u],u);
//cout<>T;
rep(kase,1,T) {
cin>>n>>m;
clear();
rep(i,1,n) {
cin>>s[i];
s[i]=s[i]+d+s[i];
}
rep(i,1,m) {
cin>>a>>b;
b=b+d+a;
len[i]=b.size();
Q[Insert(b)].push_back((int)i);
}
Build();
rep(i,1,n) {
int p=0,len=s[i].size();
rep(j,0,len-1) {
p=trie[p][s[i][j]-d];
//cout<<"to pos "<