AC自动机模板
#include<bits/stdc++.h> using namespace std; const int N=3e4+10; char str[N]; int tr[N][2],tail[N],idx; int q[N],ne[N]; void insert()//将子串连成tire { int p=0; for(int i=0;str[i];i++) { int c=str[i]-'0'; if(!tr[p][c]) tr[p][c]=++idx; p=tr[p][c]; } //定义的tail的意义来操作 tail[p]++; } void build()//将子串进行求next { int hh=0,tt=-1; for(int i=0;i<2;i++) if(tr[0][i]) q[++tt]=tr[0][i]; while(hh<=tt) { int t=q[hh++]; for(int i=0;i<2;i++) { int &p=tr[t][i]; if(!p) p=tr[ne[t]][i]; else { ne[p]=tr[ne[t]][i]; q[++tt]=p; //定义tail的意义来操作 tail[p]=tail[p]||tail[ne[p]]; } } } } bool solve(int u)//子串跟母串要操作的 { }
AC自动机=tire+kmp,AC自动机就是在tire里面建立kmp
kmp中next[i]数组的意思是在p串中,以p[i]结尾的后缀,能够匹配的从1开始的非平凡前缀的最大长度,简而言之,当前的后缀与最大的前缀匹配的最长长度
kmp中的求next的求法
用上一个位置的next更新我这一层的next
对应到tire中的kmp求法,就是用前一层的next信息更新我当前这一层的信息,然后一层一层的遍历用bfs来搜
把一维next对应到二维的next中
tire中的next数组的指向
1.搜索关键词
信息学奥赛一本通(C++版)在线评测系统 (ssoier.cn)
题意就是问最后的长的字符串中包含多少个前面输入的一系列字符串
AC自动机写法
#include<bits/stdc++.h> using namespace std; const int N=10010,S=55,M=1000010; int n; int tr[N*S][26],cnt[N*S],idx; char str[M]; int q[N*S],ne[N*S]; void insert()//tire树 { int p=0;//从头结点开始插 for(int i=0;str[i];i++) { int t=str[i]-'a';//映射到数字 if(!tr[p][t]) tr[p][t]=++idx;//假如这个位置没有这个字母,则新开辟个节点 p=tr[p][t];//跳到这个位置 } cnt[p]++;//这个位置的单词++ } void build()//求next数组的过程 { int hh=0,tt=-1; for(int i=0;i<26;i++)//把第一层的先入队 if(tr[0][i]) q[++tt]=tr[0][i]; while(hh<=tt) { int t=q[hh++];//取出队头 for(int i=0;i<26;i++)//枚举子节点 { int c=tr[t][i]; if(!c) continue;//假如这个位置没有这个节点,则直接跳 int j=ne[t]; while(j&&!tr[j][i]) j=ne[j];//假如匹配不成功则继续跳 if(tr[j][i]) j=tr[j][i];//成功就为这个位置 ne[c]=j;//这个节点的next更新一下 q[++tt]=c;//把这个节点入队 } } } int main() { int T; scanf("%d",&T); while(T--) { //清空上一层的状态 memset(tr,0,sizeof tr); memset(cnt,0,sizeof cnt); memset(ne,0,sizeof ne); idx=0; scanf("%d",&n); for(int i=0;i<n;i++) { scanf("%s",str); insert();//插入这个单词 } build();//求next数组 scanf("%s",str); int res=0; for(int i=0,j=0;str[i];i++)//next匹配的过程 { int t=str[i]-'a'; while(j&&!tr[j][t]) j=ne[j];//假如这个位置匹配不上,则继续跳 if(tr[j][t]) j=tr[j][t];//假如匹配了,则等于这个位置 int p=j; while(p)//枚举p所重复的单词 { res+=cnt[p]; cnt[p]=0;//把这个位置的单词清空 p=ne[p];//继续枚举上一个单词 } } printf("%d\n",res); } return 0; }
AC自动机优化成tire图的写法,优化常数,其实就是把不匹配的直接一步跳到匹配的地方,则下次用就不用while求匹配的位置了,可以直接用
优化的位置是求next数组的位置跟next匹配的过程
void build()//求next数组的过程 { int hh=0,tt=-1; for(int i=0;i<26;i++)//把第一层的先入队 if(tr[0][i]) q[++tt]=tr[0][i]; while(hh<=tt) { int t=q[hh++];//取出队头 for(int i=0;i<26;i++)//枚举子节点 { int p=tr[t][i]; if(!p) tr[t][i]=tr[ne[t]][i];//假如这个节点不存在,直接就跳到上一层的节点 else { ne[p]=tr[ne[t]][i];//让这个节点等于上一层的节点 q[++tt]=p;//放进队列里 } } } } for(int i=0,j=0;str[i];i++)//next匹配的过程 { int t=str[i]-'a'; j=tr[j][t];//因为优化的是直接等于next跳完后的值,所以直接跳到这个位置 int p=j; while(p)//枚举p所重复的单词 { res+=cnt[p]; cnt[p]=0;//把这个位置的单词清空 p=ne[p];//继续枚举上一个单词 } }1. void
2.修复DNA(状态机dp+AC自动机)
题意相当于给定我们一系列字符串,然后最后给我们一个长的字符串,问我们至少改变这个字符串的多少个字母使得这个长的字符串不包含前面输入的一系列字符串
状态机的dp分析
#include<stdio.h> #include<string.h> #define min(a,b) a>b?b:a const int N=1010; int n,m; int tr[N][4],dar[N],idx;//tr就是tire,dar就是标记这个单词是否存在 char str[N]; int q[N],ne[N]; int f[N][N];//f就是状态机dp的f函数 int get(char c)//把字母映射到数字里 { if(c=='A') return 0; if(c=='T') return 1; if(c=='G') return 2; return 3; } void insert()//tire的插入 { int p=0; for(int i=0;str[i];i++) { int t=get(str[i]); if(!tr[p][t]) tr[p][t]=++idx; p=tr[p][t]; } dar[p]=1;//标记这个结尾的单词是存在的 } void build()//求next数组 { int hh=0,tt=-1; for(int i=0;i<4;i++)//把第一层放进队列中 if(tr[0][i]) q[++tt]=tr[0][i]; while(hh<=tt) { int t=q[hh++];//求出队头 for(int i=0;i<4;i++)//枚举下一层的节点 { int p=tr[t][i]; if(!p) tr[t][i]=tr[ne[t]][i];//假如这个节点不存在,则直接跳到存在的节点上 else { ne[p]=tr[ne[t]][i];//存在则直接跳即可 q[++tt]=p; dar[p]|=dar[ne[p]];//把这个单词标记为危险DNA序列 } } } } int main() { int T=1; while(scanf("%d",&n),n) { //清空上一层的状态 memset(tr,0,sizeof tr); memset(dar,0,sizeof dar); memset(ne,0,sizeof ne); idx=0; for(int i=0;i<n;i++) { scanf("%s",str); insert();//插入这个单词 } build();//求next数组 scanf("%s",str+1); m=strlen(str+1); memset(f,0x3f,sizeof f);//因为要求最小值,则初始化为正无穷 f[0][0]=0;//从0个开始匹配到AC自动机第0个字母最小修改0次 for(int i=0;i<m;i++)//枚举串的长度 for(int j=0;j<=idx;j++)//枚举所有危险的单词 for(int k=0;k<4;k++)//枚举危险单词的字母 { int t=get(str[i+1])!=k;//看看这个单词是否等于第k个位置 int p=tr[j][k];//获取危险单词的这个位置的字母 if(!dar[p]) f[i+1][p]=min(f[i+1][p],f[i][j]+t);//假如不存在这个危险单词,则更新最小值 } int res=0x3f3f3f3f; for(int i=0;i<=idx;i++) res=min(res,f[m][i]);//枚举所有需要更新的最小 if(res==0x3f3f3f3f) res=-1; printf("Case %d: %d\n",T++,res); } return 0; }
3.单词
信息学奥赛一本通(C++版)在线评测系统 (ssoier.cn)
相当于问其他子串里有多少个后缀与我这个串匹配。则反向递推求这个后缀匹配多少个前缀,相当于让f[i]出现的次数累加到f[next[i]]里面,按照拓扑序递推回去next【i】然后累加就行了
#include<bits/stdc++.h> using namespace std; const int N=1000010; int n; int tr[N][26],f[N],idx;//f记录的是以这个结尾的单词的数量 int q[N],ne[N]; char str[N]; int id[210]; void insert(int x)//tire的插入 { int p=0; for(int i=0;str[i];i++) { int t=str[i]-'a'; if(!tr[p][t]) tr[p][t]=++idx; p=tr[p][t]; f[p]++;//因为记录的是前缀,则在内部就记录这个值了 } id[x]=p;// } void build() { int hh=0,tt=-1; for(int i=0;i<26;i++)//把第一层的节点放进队列中 if(tr[0][i]) q[++tt]=tr[0][i]; while(hh<=tt) { int t=q[hh++]; for(int i=0;i<26;i++)//枚举可能的子节点 { int p=tr[t][i]; if(!p) tr[t][i]=tr[ne[t]][i];//假如不存在这个节点,则直接跳他的上一个节点 else { ne[p]=tr[ne[t]][i];//直接跳上一层的节点 q[++tt]=p;//放进队列里 } } } } int main() { scanf("%d",&n); for(int i=0;i<n;i++) { scanf("%s",str); insert(i);//插入这个字符串 } build();//求next数组 for(int i=idx-1;i>=0;i--) f[ne[q[i]]]+=f[q[i]];//因为bfs搜索更新后的逆序就是拓扑序 for(int i=0;i<n;i++) printf("%d\n",f[id[i]]); return 0; }