算法思想
书本上的采用动态链表来实现,自觉不妥,遂寻得静态链表的版本
AC自动机是多模匹配算法,对于多个模式串,求主串S包含所有模式串的次数
大体步骤如下
1. 构建一棵字典树
2. 构建AC自动机
3. 进行模式匹配
字典树的构建在此不再赘述,只给出代码
void Insert()
{
int p=0,len=strlen(s);
for(int i=0;i<len;i++)
{
int ch=s[i]-'a';
if(trie[p][ch]==-1)
trie[p][ch]=++cnt;
p=trie[p][ch];
}
End[p]++;
}
AC自动机构建的关键是失配指针的使用,模式串在字典树上匹配失败时,会跳转到当前节点失配指针所指向的节点,再次匹配,AC自动机是由字典树及失配指针组成的,在字典树创建完成后再给每个节点添加失配指针,AC自动机便构造完成,即失配指针为匹配失败后的跳转位置,AC自动机的失配指针指向的节点代表的字符串为当前节点代表的字符串的后缀,且在字典树中没有更长的当前及节点的后缀,具体看代码和注释
构建过程
1. 树根入队
2. 若队列不为空,取队首并弹出,访问子节点trie[p][i],子节点为空trie[p][i]指向trie[fail[p]][i], 不为空子节点的失配指针指向父节点的失配的对应子节点,即trie[fail[p]][i]
3. 队空结束
代码如下:
void Build()
{
queue<int>Q;
for(int i=0;i<26;i++)
if(trie[0][i]==-1)
trie[0][i]=0;
else
{
fail[trie[0][i]]=0;
Q.push(trie[0][i]);
}
while(!Q.empty())
{
int t=Q.front();
Q.pop();
for(int i=0;i<26;i++)
if(trie[t][i]==-1)
trie[t][i]=trie[fail[t]][i];
else
{
fail[trie[t][i]]=trie[fail[t]][i];
Q.push(trie[t][i]);
}
}
}
模式匹配指长树根开始处理模式串中的每个字符,沿着当前字符的fail指针,一直遍历到所有单词已经遍历过为止
代码如下
int Query() {
int t=0,len=strlen(s);
int acc=0;
for(int i=0; i<len; i++) {
t=trie[t][s[i]-'a'];
for(int j=t; j&&cnt[j]!=-1; j=fail[j]) {
//一直向下找,直到匹配失败
acc+=cnt[j];
cnt[j]=-1;//标记已经访问
}
}
return acc;
}
训练
HDU2222
题目大意:给出数个模式串和一个主串,判断主串中有多少种模式串
思路:AC自动机的基本用法,直接用模板即可
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
int trie[500010][26],cnt[500010],fail[500010],T,n,ans;//内存需要开的很大
char s[1212121];
void Insert() {//构造字典树
int p=0,len=strlen(s);
for(int i=0; i<len; i++) {
int ch=s[i]-'a';
if(!trie[p][ch])
trie[p][ch]=++ans;
p=trie[p][ch];
}
cnt[p]++;
}
void Pre() {
queue<int>Q;
for(int i=0; i<26; i++)
if(trie[0][i]) {//单独处理第一层
fail[trie[0][i]]=0;//指向根,也可以去掉,因为默认为0
Q.push(trie[0][i]);//按入第一层
}
while(!Q.empty()) {
int t=Q.front();
Q.pop();
for(int i=0; i<26; i++) {
if(trie[t][i]) {
fail[trie[t][i]]=trie[fail[t]][i];
/*如果有子节点i+'a',让子节点失败指针指向父节点的失败指针
对应字母的节点*/
Q.push(trie[t][i]);
} else
trie[t][i]=trie[fail[t]][i];
//否则让子节点指向当前节点t的fial指针的子节点
}
}
}
long long Query() {
Pre();
int t=0,len=strlen(s);
long long acc=0;
for(int i=0; i<len; i++) {
t=trie[t][s[i]-'a'];
for(int j=t; j&&cnt[j]!=-1; j=fail[j]) {
//一直向下找,直到匹配失败
acc+=cnt[j];
cnt[j]=-1;//标记已经访问
}
}
return acc;
}
int main() {
scanf("%d",&T);
while(T--) {
scanf("%d",&n);
for(int i=0; i<n; i++) {
scanf("%s",s);
Insert();
}
scanf("%s",s);
printf("%lld\n",Query());
memset(trie,0,sizeof(trie));
memset(cnt,0,sizeof(cnt));
memset(fail,-1,sizeof(fail));
ans=0;
}
return 0;
}
HDU2896
题目大意:略
思路:AC自动机变化用法,需要修改相关函数,具体见代码
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <queue>
using namespace std;
int N,M,trie[100001][130],cnt[100001],fail[100001],ans;//字典树(ascall码为基准),单词编号,失配指针
bool visit[100001];//标记访问数组
char origin[100001];
void Insert(int t) {//需要记录单词编号
int p=0,len=strlen(origin);
for(int i=0; i<len; i++) {
int ch=origin[i];
if(!trie[p][ch])
trie[p][ch]=++ans;
p=trie[p][ch];
}
cnt[p]=t;//记录编号
}
void Pre() {
queue<int>Q;
for(int i=0; i<=128; i++)
if(trie[0][i])
Q.push(trie[0][i]);
while(!Q.empty()) {
int t=Q.front();
Q.pop();
for(int i=0; i<=128; i++)
if(trie[t][i]) {
fail[trie[t][i]]=trie[fail[t]][i];
Q.push(trie[t][i]);
} else
trie[t][i]=trie[fail[t]][i];
}
}
bool Query(priority_queue<int,vector<int>,greater<int>>&Q) {//因为需要排序,所以用堆
int t=0,len=strlen(origin);
for(int i=0; i<len; i++) {
t=trie[t][origin[i]];
for(int j=t; j&&!visit[j]; j=fail[j])
if(cnt[j]) {
Q.push(cnt[j]);
visit[j]=1;//不能更改单词编号,只需标记访问
}
}
return !Q.empty();
}
int main() {
scanf("%d",&N);
for(int i=1; i<=N; i++) {
scanf("%s",origin);
Insert(i);
}
scanf("%d",&M);
ans=0;
Pre();//只需执行一次
for(int i=1; i<=M; i++) {
scanf("%s",origin);
memset(visit,0,sizeof(visit));
priority_queue<int,vector<int>,greater<int>>Q;
if(Query(Q)) {
printf("web %d:",i);
while(!Q.empty()) {
printf(" %d",Q.top());
Q.pop();
}
ans++;
putchar('\n');
}
}
printf("total: %d\n",ans);
return 0;
}
POJ2778
题目大意:DNA片段只包括ACTG,给出m个遗传病片段,求有多少种长度为n的DNA序列不包含这些片段
思路:构建遗传病片段字典树,构建AC自动机,构建时,若当前及诶点的失败指针有结束标记,则对当前节点也标记,构建林姐矩阵,对所有未标记的节点都重新编号,根据AC自动机创建邻接矩阵,求解矩阵n次幂
关于答案与矩阵的关系
假设片段为{ACG,C},可得DFA,红色代表不能到达的状态
从状态0出发走1步有四种走法,但是可取的走法只有三种,所以n=1时,答案为3,同理,n=2时,从状态0走2步形成一个长度为2的字符串,只要在路径上没有经过危险节点,则有几种走法,答案就为几,因此可以构造出矩阵
M
M
M
[
2
1
0
0
1
2
1
1
0
0
1
1
0
1
1
2
1
0
0
1
2
1
0
0
1
]
\begin{bmatrix} 2&1&0&0&1\\ 2&1&1&0&0\\ 1&1&0&1&1\\ 2&1&0&0&1\\ 2&1&0&0&1\\ \end{bmatrix}
⎣⎢⎢⎢⎢⎡2212211111010000010010111⎦⎥⎥⎥⎥⎤
M
[
i
,
j
]
M[i,j]
M[i,j]表示从节点i到j只走1步有几种走法(i从0开始),
M
n
M^n
Mn表示从节点i到j走n步有多少种走法
需要注意的是,要去掉危险的行和列,状态3和4是遗传病片段的结尾,是不可达状态,节点2的失败指针指向4,匹配了AC的时候也匹配了C,所以2也不可达,去掉对应行列后可得矩阵M
[
2
1
2
1
]
\begin{bmatrix} 2&1\\ 2&1 \end{bmatrix}
[2211]
由于n很大,直接使用矩阵快速幂
代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
using namespace std;
int trie[2000001][5],End[2000001],fail[2000001],m,n,MOD=1e5,cnt,id[2000001];
long long ans[200][200],A[200][200];
char input[11];
void Mul(bool flag) {//矩阵乘法
long long C[200][200];
memset(C,0,sizeof(C));
for(int i=0; i<cnt; i++)
for(int j=0; j<cnt; j++)
for(int k=0; k<cnt; k++)
if(flag)
C[i][j]=(C[i][j]+ans[i][k]*A[k][j])%MOD;
else
C[i][j]=(C[i][j]+A[i][k]*A[k][j])%MOD;
for(int i=0; i<cnt; i++)
for(int j=0; j<cnt; j++)
if(flag)
ans[i][j]=C[i][j];
else
A[i][j]=C[i][j];
}
void Pow() {
for(int i=0; i<cnt; i++)
ans[i][i]=1;
while(n) {
if(n&1)
Mul(1);
Mul(0);
n>>=1;
}
}
void Insert() {
int len=strlen(input),p=0;
for(int i=0; i<len; i++) {
int ch;
switch(input[i]) {//获得位置
case 'A':
ch=0;
break;
case 'C':
ch=1;
break;
case 'T':
ch=2;
break;
case 'G':
ch=3;
break;
}
if(trie[p][ch]==-1)//如果该位置为空,开辟
trie[p][ch]=++cnt;
p=trie[p][ch];
}
End[p]++;//记录该位置有单词
}
void Build() {
queue<int>Q;
for(int i=0; i<4; i++)//对第一层特殊处理
if(trie[0][i]==-1)
trie[0][i]=0;
else {
fail[trie[0][i]]=0;
Q.push(trie[0][i]);
}
while(!Q.empty()) {
int t=Q.front();
Q.pop();
if(End[fail[t]])//当前节点的fail有end,end++
//该点的失败指针为单词代表匹配到该点时,失败指针的序列已经被匹配
End[t]++;
for(int i=0; i<4; i++)//构造fail
if(trie[t][i]!=-1) {
fail[trie[t][i]]=trie[fail[t]][i];
Q.push(trie[t][i]);
} else
trie[t][i]=trie[fail[t]][i];
}
}
long long Query() {
int ids=0;
memset(id,-1,sizeof(id));
for(int i=0; i<cnt; i++)
if(!End[i])//重新编号节点,即可走的节点
id[i]=ids++;
for(int i=0; i<cnt; i++) {
if(End[i])
continue;
for(int j=0; j<4; j++) {//构造矩阵,A[i][j]代表从状态i到j的方法数
int v=trie[i][j];
if(!End[v])
A[id[i]][id[v]]++;
}
}
cnt=ids;
Pow();//矩阵快速幂
long long res=0;
for(int i=0; i<cnt; i++)//记录0状态到各状态的方法总和
res=(res+ans[0][i])%MOD;
return res;
}
int main() {
scanf("%d%d",&m,&n);
if(m!=0) {
memset(trie,-1,sizeof(trie));
for(int i=0; i<m; i++) {
scanf("%s",input);
Insert();//字典树构建
}
Build();
printf("%lld",Query());
} else {//特判m为0,此时为乘法原理
long long res=1,p=4;
while(n) {
if(n&1)
res=res*p%MOD;
p=p*p%MOD;
n>>=1;
}
printf("%lld",res);
}
return 0;
}
HDU2243
题目大意:略
思路:本题与poj2778有两个不同之处:
- 本题长度不超过L,poj2778长度为n
- 本期求解的是至少包含一个词根的单词数,poj2778求解的是不包含遗传病片段的DNA序列数
本题首先求解有26个小写字母组成且不超过L的单词数ans,求解不超过L且不包含词根的单词数res,最后求两者差
关于矩阵
对等比矩阵求和的经典算法(A为矩阵)
[
A
E
0
E
]
\begin{bmatrix} A&E\\ 0&E \end{bmatrix}
[A0EE]
n次幂后
[
A
n
1
+
A
1
+
A
2
+
⋯
+
A
n
−
1
0
E
]
\begin{bmatrix} A^n&1+A^1+A^2+\dots+A^{n-1}\\ 0&E \end{bmatrix}
[An01+A1+A2+⋯+An−1E]
本题把A设置为26,可以得到
[
26
1
0
1
]
\begin{bmatrix} 26&1\\ 0&1 \end{bmatrix}
[26011]
进行n次幂
[
2
6
n
1
+
2
6
1
+
2
6
2
+
⋯
+
2
6
n
−
1
0
1
]
\begin{bmatrix} 26^n&1+26^1+26^2+\dots+26^{n-1}\\ 0&1 \end{bmatrix}
[26n01+261+262+⋯+26n−11]
因此对矩阵第一行累和-1可得到总数量
对于求不包含词根的单词个数,由输入样例{aa,ab}可得矩阵
[
25
1
24
0
]
\begin{bmatrix} 25&1\\ 24&0 \end{bmatrix}
[252410]
为实现累加,需要在原矩阵最后一行+0,最后一列+1得到
[
25
1
1
24
0
1
0
0
1
]
\begin{bmatrix} 25&1&1\\ 24&0&1\\ 0&0&1 \end{bmatrix}
⎣⎡25240100111⎦⎤
求出n次幂第一行之和-1得到方案数
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef unsigned long long ull;
typedef struct square {
ull data[200][200],n;
square(ull _n) {
memset(data,0,sizeof(data));
n=_n;
}
square& operator=(square a) {
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
data[i][j]=a.data[i][j];
return *this;
}
} square;//设置矩阵结构体方便各操作
square A(2);
int N,L,trie[200001][26],fail[200001],End[200001],cnt,id[200001];//内存不宜开过大
ull ans,ids;
char input[20];
square Mul(square& a,square& b) {//矩阵乘法
square tmp(a.n);
for(int i=0; i<a.n; i++)
for(int j=0; j<a.n; j++)
for(int k=0; k<a.n; k++)
tmp.data[i][j]+=a.data[i][k]*b.data[k][j];
return tmp;
}
void Pow(square&t,ull n) {//快速幂
square tmp(t.n);
for(int i=0; i<t.n; i++)//单位矩阵
tmp.data[i][i]=1;
while(n) {
if(n&1)
tmp=Mul(tmp,t);
t=Mul(t,t);
n>>=1;
}
t=tmp;
}
ull Query() {
for(int i=0; i<cnt; i++)
if(!End[i])
id[i]=ids++;
square B(ids+1);
for(int i=0; i<cnt; i++) {
if(End[i])
continue;
for(int j=0; j<26; j++) {
int v=trie[i][j];
if(!End[v])
B.data[id[i]][id[v]]++;
}
}
ull res=0;
for(int i=0; i<=ids; i++)
B.data[i][ids]=1;
Pow(B,L);
for(int i=0; i<ids+1; i++)
res+=B.data[0][i];
return --res;//去掉1,如果两个都不去1,结果对,但是逻辑上不成立
}
void Insert() {//构造字典树
int p=0,len=strlen(input);
for(int i=0; i<len; i++) {
int ch=input[i]-'a';
if(trie[p][ch]==-1)
trie[p][ch]=++cnt;
p=trie[p][ch];
}
End[p]++;
}
void Build() {
queue<int>Q;
for(int i=0; i<26; i++)
if(trie[0][i]==-1)
trie[0][i]=0;
else {
fail[trie[0][i]]=0;
Q.push(trie[0][i]);
}
while(!Q.empty()) {
int t=Q.front();
Q.pop();
if(End[fail[t]])//上一题
End[t]++;
for(int i=0; i<26; i++)
if(trie[t][i]==-1)
trie[t][i]=trie[fail[t]][i];
else {
fail[trie[t][i]]=trie[fail[t]][i];
Q.push(trie[t][i]);
}
}
}
int main() {
while(~scanf("%d%d",&N,&L)) {
memset(trie,-1,sizeof(trie));
memset(id,0,sizeof(id));
memset(End,0,sizeof(End));
memset(fail,0,sizeof(fail));
cnt=0;
ans=ids=0;
for(int i=0; i<N; i++) {
scanf("%s",input);
Insert();
}
Build();
A.data[0][0]=26,A.data[0][1]=A.data[1][1]=1;
Pow(A,L);//快速幂计算总数量
ans=A.data[0][0]+A.data[0][1]-1;
printf("%llu\n",ans-Query());
}
return 0;
}
2016 香港区域赛Tabbo
题目大意:给出n个01串,求出一个最长的01串s使得这n个01串都不为s的子串
思路:直接构造AC自动机,用DFS查找构造满足条件的字符串并且标记已经访问过的节点,如果在搜索过程中访问到已标记,代表可以组成无限个,有个与基本AC自动机不同的地方:对于一个节点,当需要向下遍历时,需要判断当前节点+一个子节点是否会匹配了输入串,需要从子的fail出发一直向下,如果碰到一个节点是串的结尾说明会匹配输入串,则该子节点不可选,那么反向推理,如果一个节点x是一个串的结尾且该节点x又是另一个节点y的失配位置,那么显然y节点到x的这条道也应当不可选了
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=4e5+10;
int trie[maxn][2],End[maxn],fail[maxn],n,cnt,length;
bool vis[maxn];
char s[maxn],res[2][maxn];
void Insert() {
int p=0,len=strlen(s);
for(int i=0; i<len; i++) {
int ch=s[i]-'0';
if(!trie[p][ch])
trie[p][ch]=++cnt;
p=trie[p][ch];
}
End[p]++;
}
void Build() {
queue<int>Q;
for(int i=0; i<2; i++)
if(trie[0][i])//预处理第一层
Q.push(trie[0][i]);
while(!Q.empty()) {
int t=Q.front();
Q.pop();
for(int i=0; i<2; i++)
if(trie[t][i]) {//如果不为空
fail[trie[t][i]]=trie[fail[t]][i];
End[trie[t][i]]+=End[trie[fail[t]][i]];
//这里如果在t这里失配但是在t的失配指针匹配,也算t能够匹配
Q.push(trie[t][i]);
} else
trie[t][i]=trie[fail[t]][i];
}
}
bool DFS(int x,int dep) {
if(vis[x])return 1;//如果访问过,代表有环
vis[x]=1;
bool flag,t=0;
for(int i=0; i<2; i++) {
flag=1;
int id=trie[x][i];//获得下一位置
if(id&&End[id]>0)flag=0;//如果不为空且存在匹配,不能取
if(flag) {
res[0][dep]=i+'0';//构造
if((flag=DFS(id,dep+1)))return 1;//继续构造,存在环返回
}
t|=flag;//记录是否有环
}
if(!t&&dep>length) {//无环且比当前字符串长度长
length=dep;
for(int j=0; j<length; j++)res[1][j]=res[0][j];
}
vis[x]=0;
return flag;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++) {
cin >>s;
Insert();//字典树插入
}
Build();//建立AC自动机
if(DFS(0,0))//搜索字符串
cout <<-1;
else
for(int i=0; i<length; i++)cout <<res[1][i];//输出结果
return 0;
}
总结
AC自动机是结合了字典树和KMP思想的算法,可以和多种算法搭配,如矩阵快速幂,树链剖分等,是较难的算法,较为模板化
1404





