一.前言
AC自动机是一种字符串匹配的算法,用来将多个模板串t与文本串s匹配。例如:求解文本串中包含了多少个模板串
其前置知识是:KMP和字典树
二.前置知识字典树
1.基本概念
字典树是一棵包含所有模板串的26叉树。
树的”边权“是字符,从根节点出发到任意点的”距离“,即为某一字符串的前缀。
2.建立字典树
指针版
用next[26]指针来表示每一叉所指向的26子叉
用fail存储每个节点的失配情况(不懂没关系)
用sum表示这个节点是不是一个单词的结尾,以及相应的个数
struct node {
node *next[26];//每一个字符子叉
node *fail;//实配指针
int cntword;//表示是否是单词结尾,以及相应的个数
};
queue<node*>q;
char t[1000005];//模板串
char s[1000005];//文本串
node *root;//字典树的根
//建立字典树
void insertWords(char *t) {
node *p=root;
for(int i=0; t[i]; i++) {
int x=t[i]-'a';
if(p->next[x]==NULL) {
node *newnode=(struct node *)malloc(sizeof(struct node));//新建一个点
for(int j=0; j<26; j++)newnode->next[j]=0;
newnode->cntword=0;
newnode->fail=0;
p->next[x]=newnode;
}
p=p->next[x];
}
p->cntword++;//当前节点单词数+1
}
数组版
int trie[N][26];//字典树trie
int cntword[N];//计算该单词出现次数
int fail[N];//失配指针
int cnt;//动态开点
void insertWords(char *t){
int now=0;
for(int i=0;t[i]!='\0';i++){
int next=t[i]-'a';
if(!trie[now][next])trie[now][next]=cnt;
now=trie[now][next];
}
cntword[now]++;//当前单词数+1
}
三.前置知识KMP(运用到其思想)
1.基本概念
KMP是一种匹配单一模板串t和文本串s的算法
即判断:模板串是否为文本串的子串
2.算法实现
暴力:枚举模板串在文本串的开始位置即可
int force(string s,string t){
//从目标串s中匹配模板串t
int i=0,j=0;
while(i<s.length()&&j<t.length()){
if(s[i]==t[j])i++,j++;
else i=i-j+1,j=0;//每次都要完整的重新比较,即s串向右移一位,t串从0重新开始
}
if(j==t.length())return i-j;
else return -1;
}
如何优化?KMP
每次都重新匹配时间复杂度太高了,我们能否在每次匹配失败的时候,只让模板串的t指针回到一个恰当的位置,使得该位置前的所有字符串已匹配成功。即维护一个next[]表示每次失配时模板串t需要回到的位置。
int KMP(string s,string t){
//从目标串s中匹配模板串t
KMP_init(t);
int i=0,j=0;
while(i<s.length()&&j<t.length()){
if(s[i]==t[j]||j==-1)i++,j++;
else j=next[j];//[0,next[j]-1]匹配完成,t串直接回退到next[j]位置即可
}
if(j==t.length())return i-j;
else return -1;
}
如何预处理这个模板串t的next数组???
当模板记吧
我也不会,等我会了再讲
void KMP_init(string t){
int j=0,k=-1;
next[0]=-1;
while(j<t.length()-1){
if(t[j]==t[k]||k==-1){
//j指向模板串t当前正常维护的元素下标,k
next[++j]=++k;
}
else k=next[k];
}
}
四.AC自动机
1.基本理解
KMP是个一维数组,next[j]为每个模板串t的第j个字符的失配指针
那么AC自动机,就是在字典树上,求每个位置的失配指针
2.算法实现
需要实现的操作有:
1.建立字典树(上面已写)
2.建立失配指针(俺也不会,当模板敲)
3.ac_automaton(俺还是不会,当模板敲)
代码奉上(指针版)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
struct node {
node *next[26];//每一个字符子叉
node *fail;//实配指针
int cntword;//表示是否是单词结尾,以及相应的个数
};
queue<node*>q;
char t[1000005];//模板串
char s[1000005];//文本串
node *root;//字典树的根
//建立字典树
void insertWords(char *t) {
node *p=root;
for(int i=0; t[i]; i++) {
int x=t[i]-'a';
if(p->next[x]==NULL) {
node *newnode=(struct node *)malloc(sizeof(struct node));//新建一个点
for(int j=0; j<26; j++)newnode->next[j]=0;
newnode->cntword=0;
newnode->fail=0;
p->next[x]=newnode;
}
p=p->next[x];
}
p->cntword++;//当前节点单词数+1
}
//建立字典树每个节点的失配指针fail
void build_fail() {
q.push(root);
node *p;
node *temp;
while(!q.empty()) {
temp=q.front();
for(int i=0; i<26; i++) {
if(temp->next[i]) {
if(temp==root)temp->next[i]->fail=root;
else {
p=temp->fail;
while(p) {
if(p->next[i]) {
temp->next[i]->fail=p->next[i];
break;
}
p=p->fail;
}
if(p==NULL)temp->next[i]->fail=root;
}
q.push(temp->next[i]);
}
}
q.pop();
}
}
void ac_automaton(char *s) {
node *p=root;
int ans=0;
for(int i=0;s[i]!='\0'; i++) {
int x=s[i]-'a';
while(p->next[x]==NULL&&p!=root)p=p->fail;
p=p->next[x];
if(p==NULL)p=root;
node *temp=p;
while(temp!=root) {
if(temp->cntword>=0) {
ans+=temp->cntword;
temp->cntword=-1;//将遍历过后的节点标记,防止重复计算
} else break;
temp=temp->fail;
}
}
return ans;
}
int main() {
int T,n;
cin>>T;
while(T--) {
//初始化字典树树根
root=(struct node*)malloc(sizeof(struct node));
for(int i=0; i<26; i++)root->next[i]=0;
root->fail=0;
root->cntword=0;
//建立字典树trie
cin>>n;
for(int i=1; i<=n; i++) {
cin>>t;
insertWords(t);
}
//建立字典树的失配指针
build_fail();
//AC自动机自动匹配
cin>>s;
cnt=0;
cout<<ac_automaton(s)<<endl;
}
return 0;
}
代码奉上(数组版)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int trie[N][26];//字典树trie
int cntword[N];//计算该单词出现次数
int fail[N];//失配指针
int cnt;//动态开点
string s;//文本串
string t;//模板串
queue<int>q;//bfs求fail的时候用到
void insertWords(string t){
int now=0;
for(int i=0;t[i]!='\0';i++){
int next=t[i]-'a';
if(!trie[now][next])trie[now][next]=++cnt;
now=trie[now][next];
}
cntword[now]++;//当前单词数+1
}
void build_fail(){
for(int i=0;i<26;i++){
if(trie[0][i]){
fail[trie[0][i]]=0;//第二层的点失配指针指向根节点
q.push(trie[0][i]);//第二层的点扔进队列中
}
}
while(!q.empty()){
int now=q.front();
for(int i=0;i<26;i++){
//如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点
if(trie[now][i]){
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
}
//如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边
else {
trie[now][i]=trie[fail[now]][i];
}
}
q.pop();
}
}
int ac_automaton(string s){
int now=0,ans=0;
for(int i=0;s[i]!='\0';i++){
//遍历文本串
now=trie[now][s[i]-'a'];//从s[i]开始寻找
//一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过)
for(int j=now;j!=0&&cntword[j]!=-1;j=fail[j]){
ans+=cntword[j];
cntword[j]=-1;//防止重复计算
}
}
return ans;
}
int main(){
int T,n;
cin>>T;
while(T--){
memset(cntword,0,sizeof(cntword));
memset(fail,0,sizeof(fail));
memset(trie,0,sizeof(trie));
cin>>n;
for(int i=1;i<=n;i++){
cin>>t;
insertWords(t);
}
build_fail();
cin>>s;
cout<<ac_automaton(s)<<endl;
}
return 0;
}
3.开始刷题
题目大意:求出现在文本串中最多次的模板串
题解:
不存在重复字符串,且每个字符串可以重复计数
1.那么到每个叶子节点的字符串cntword始终为1,且到trie的每一个前缀,要么为1,要么为0。
2.因为每个模板串都不止被文本串走1次,故在ac_automaton的时候,不能标记cntword为-1来防止下一次经过
3.为了记录每个模板串的出现次数和字符串的本身,需要一个结构体存储,并排序。在ac_automaton的时候记录每一个出现的模板串的出现次数和模板编号。
4.为了实现2的模板编号查找,模板串需离线,且我们需要有一个mark数组存储每一个insertwords的时候的叶节点所对应模板编号。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int trie[N][26];
int cntword[N];
int fail[N];
int cnt;
int mark[N];
int vis[N];
string s;
string t[160];
queue<int>q;
struct ppp {
int pos,num;
} ans[160];
int top;
int cmp(ppp x,ppp y) {
if(x.num==y.num)return x.pos<y.pos;
else return x.num>y.num;
}
void insertWords(string t,int pos) {
int now=0;
for(int i=0; t[i]!='\0'; i++) {
int next=t[i]-'a';
if(!trie[now][next])trie[now][next]=++cnt;
now=trie[now][next];
}
cntword[now]++;
mark[now]=pos;
}
void build_fail() {
for(int i=0; i<26; i++) {
if(trie[0][i]) {
fail[trie[0][i]]=0;
q.push(trie[0][i]);
}
}
while(!q.empty()) {
int now=q.front();
for(int i=0; i<26; i++) {
if(trie[now][i]) {
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
} else {
trie[now][i]=trie[fail[now]][i];
}
}
q.pop();
}
}
void ac_automaton(string s) {
int now=0;
for(int i=0; s[i]!='\0'; i++) {
now=trie[now][s[i]-'a'];
for(int j=now; j!=0; j=fail[j]) {
if(vis[mark[j]]==0) {
ans[++top].num=cntword[j];
ans[top].pos=mark[j];
vis[mark[j]]=top;
}
else {
ans[vis[mark[j]]].num+=cntword[j];
}
}
}
sort(ans+1,ans+top+1,cmp);
int maxx=ans[1].num;
cout<<maxx<<endl;
for(int i=1; i<=top; i++)if(ans[i].num==maxx)cout<<t[ans[i].pos]<<endl;
}
int main() {
int n;
while(1) {
cin>>n;
if(n==0)return 0;
memset(cntword,0,sizeof(cntword));
memset(fail,0,sizeof(fail));
memset(trie,0,sizeof(trie));
memset(ans,0,sizeof(ans));
memset(vis,0,sizeof(vis));
top=0;
for(int i=1; i<=n; i++) {
cin>>t[i];
insertWords(t[i],i);
}
build_fail();
cin>>s;
ac_automaton(s);
}
return 0;
}
题目大意:求每个模板串在文本串中出现的次数
题解:
1.题目不保证模板串不一致,需进行一些操作,防止重复insert,这里采用的是map。
2.因为要求每个模板串出现的次数,所以要记下每个模板串在trie树上的终止节点
3.暴力跑fail边很好想但是T,因为会被类似aaaa的数据卡掉
void ac_automaton(string s) {
int now=0;
for(int i=0; s[i]!='\0'; i++) {
now=trie[now][s[i]-'a'];
for(int j=now; j!=0; j=fail[j]) {
num[j]+=cntword[j];
}
}
for(int i=1; i<=n; i++)cout<<num[mark[i]]<<endl;
}
4.正解:建出 fail 树,记录自动机上的每个状态被匹配了几次,最后求出每个模式串在 Trie 上的终止节点在 fail 树上的子树总匹配次数就可以了。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int trie[N][26];
int cntword[N];
int fail[N];
int cnt,n;
int size[N];
int mark[N];
int fanmark[N];
string s;
string t;
queue<int>q;
map<string,int>m;
struct ppp{
int u,v,next;
}e[N*2];
int vex[N],k;
void add(int u,int v){
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
}
void insertWords(string t,int pos) {
int now=0;
for(int i=0; t[i]!='\0'; i++) {
int next=t[i]-'a';
if(!trie[now][next])trie[now][next]=++cnt;
now=trie[now][next];
}
cntword[now]++;
mark[pos]=now;
fanmark[now]=pos;
}
void build_fail() {
for(int i=0; i<26; i++) {
if(trie[0][i]) {
fail[trie[0][i]]=0;
q.push(trie[0][i]);
}
}
while(!q.empty()) {
int now=q.front();
for(int i=0; i<26; i++) {
if(trie[now][i]) {
fail[trie[now][i]]=trie[fail[now]][i];
q.push(trie[now][i]);
} else {
trie[now][i]=trie[fail[now]][i];
}
}
q.pop();
}
}
void dfs(int u){
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
dfs(v);
size[u]+=size[v];
}
}
void ac_automaton(string s) {
int now=0;
for(int i=0; s[i]!='\0'; i++) {
now=trie[now][s[i]-'a'];
size[now]++;
}
for(int i=1;i<=cnt;i++)add(fail[i],i);
dfs(0);
for(int i=1; i<=n; i++)cout<<size[mark[i]]<<endl;
}
int main() {
cin>>n;
for(int i=1; i<=n; i++) {
cin>>t;
if(m[t])mark[i]=mark[m[t]];
else {
m[t]=i;
insertWords(t,i);
}
}
build_fail();
cin>>s;
ac_automaton(s);
return 0;
}