AC自动机
AC自动机
AC自动机是以 \(Trie\) 的结构为基础,结合 \(KMP\) 的思想建立的自动机,用于解决多模式串(作为子串的串)匹配等任务。
-
建\(tire\) 树,正常操作即可
-
建\(fail\)树,如果当前节点失配,可以通过跳\(fail\) 快速转到一个可能有答案的位置,相当于\(kmp\)但是在树上考虑所有模式串的情况下,一段最大的前缀等于当前节点对应字符串的后缀。概念很好理解,问题在如何实现。下面是优化后的实现,不易被卡。
while(!q.empty()) { int u=q.front(); q.pop(); for(int i=0;i<=25;i++) { if(!sh[u][i])sh[u][i]=sh[fail[u]][i]; //如果不存在当前点(失配),找它父亲u失配后的节点fail[u],将边连向fail[u]的这个儿子。可能觉得fail[u]不一定有这个儿子,fail[u]一定比u先被处理,所以sh[fail[u]][i]一定处理过指向存在的一个点,且这个点一定是我要的答案。 else { q.push(sh[u][i]); int v=fail[u]; fail[sh[u][i]]=sh[v][i];//存在当前点直接fail记为sh[fail[u]][i],道理与上面相同。 } } }
像上面那样实现,不存在的点不需要暴力跳\(fail\) 可以直接用
例题:
裸板子,暴力跳\(fail\)即可。
#include <bits/stdc++.h>
using namespace std;
int n,sh[1000010][27],ans,tot,cnt[1000010],fail[1000010];
char ss[1000010];
void insert()
{
scanf("%s",ss+1);
int len=strlen(ss+1);
int u=0;
for(int i=1;i<=len;i++)
{
if(!sh[u][ss[i]-'a'])
sh[u][ss[i]-'a']=++tot;
u=sh[u][ss[i]-'a'];
}
cnt[u]++;
return ;
}
void getfail()
{
queue<int> q;
for(int i=0;i<=25;i++)
{
if(sh[0][i])
{
fail[sh[0][i]]=0;
q.push(sh[0][i]);
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<=25;i++)
{
if(!sh[u][i])sh[u][i]=sh[fail[u]][i];
else
{
q.push(sh[u][i]);
int v=fail[u];
fail[sh[u][i]]=sh[v][i];
}
}
}
}
void find()
{
int u=0,len=strlen(ss+1);
for(int i=1;i<=len;i++)
{
int k=sh[u][ss[i]-'a'];
while(k&&cnt[k]!=-1)
{
ans+=cnt[k];
cnt[k]=-1;
k=fail[k];
}
u=sh[u][ss[i]-'a'];
}
return ;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
insert();
getfail();
fail[0]=0;
scanf("%s",ss+1);
find();
printf("%d\n",ans);
return 0;
}
与上一题相同,记录每个模式串的个数即可。
看似与前题相似,只需要处理相同字符串即可,交一发就发现会TLE。
为什么会出现这种情况?前两道题自动机上的每个点都只会对答案产生一次贡献,只计算一次,这道题,可能有多次计算,复杂度最坏为\(O(模式串长度 · 文本串长度)\)
考虑如何让本体与前几道题一样只经过一次,显然我们可以打上标记,标记每个点被询问多少次,不急着跳\(fail\) .最后从深度最深的点开始跳\(fail\) 累加更新,每个点只算一次。考虑\(fail\) 树是一个\(DAG\),用\(topu\) 确定顺序即可。
void topu()
{
queue<int> q;
for(int i=0;i<=tot;i++)
if(in[i]==0)q.push(i);
while(!q.empty())
{
int u=q.front();q.pop();
int l=bo[u].size();
if(l)
{
int cur=0;
while(cur<l)
{
cnt[bo[u][cur]]=siz[u];
cur++;
}
}
int v=nxt[u];
in[v]--; //提前保存入度
siz[v]+=siz[u];
if(in[v]==0)q.push(v);
}
}
首先有一个显然的\(dp\)
复杂度为\(o(m|t|^3)\),完全过不去。
因为\(i,j\) 段大概率不是单词,所以对答案没贡献,我们要思考准确定位一个后缀是单词,跳\(ACAM\) 的的\(fail\)可以找所有后缀,我们跳\(fail\) 的同时判断他是否是单词结尾转移,复杂度可以达到\(O(m|s||t|)\),任然过不去。
观察到\(|s|\le 20\),是一个特别小的数据,可以直接状压为一个实数。对每个点储存这个点后缀是单词的长度集合。
做文本串第\(i\)位匹配时,\(x\)状压前\(i-1\)的\(f[i]\) (\(f[i]\)表示前i位是否都可以被理解,可以为1,不可以为0)
#include <bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,m,cnt,sh[4010][30],l[4010],fail[4010],g[4010],f[N];
char s[N];
bool bk[4010];
void insert(int k)
{
int u=0;
int len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int c=s[i]-'a';
if(!sh[u][c])sh[u][c]=++cnt;
u=sh[u][c];
}
bk[u]=1;
l[u]=len;
return ;
}
void getfail()
{
queue<int> q;
for(int i=0;i<=25;i++)
if(sh[0][i])q.push(sh[0][i]),fail[sh[0][i]]=0;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<=25;i++)
{
if(!sh[u][i])sh[u][i]=sh[fail[u]][i];
else
{
q.push(sh[u][i]);
fail[sh[u][i]]=sh[fail[u]][i];
}
}
}
for(int i=1;i<=cnt;i++)
{
int j=i;
while(j)
{
if(bk[j]&&l[j]<=20)
g[i]|=(1<<(l[j]-1));
j=fail[j];
}
}
}
int find()
{
int len=strlen(s+1),x=0,u=0;
for(int i=1;i<=len;i++)
{
int c=s[i]-'a';
u=sh[u][c];
x=((x<<1)|f[i-1])&((1<<20)-1);//x的第0位存的是i-1,第1位存的是i-2,...
f[i]=(x&g[u])!=0;//g[u]的第0位存后缀长度为1是否w为单词...,g[u]与x的相同位置是对应,拼起来是s,x&g[u]!=0,说明有一位个g[u]和x都为1,这个长度断开符合题目要求
}
for(int i=len;i>=1;i--)
if(f[i])return i;
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
insert(i);
}
getfail();f[0]=1;
while(m--)
{
scanf("%s",s+1);
printf("%d\n",find());
}
return 0;
}
第一眼暴力,用打印的字符串建\(ACAM\) 询问的时候从\(0\)到\(y\) 的每个点暴力跳\(fail\) 统计\(x\) 的个数。
时间复杂度显然过不去。考虑刚才实际在干什么,一条路径上的点跳\(fail\) 找\(x\),在\(fail\) 树上看,只有\(x\)的子树内在\(y\)路径上的点有贡献。将根到\(y\)的路径的值设为1,统计\(x\)的子树和。
但还是不行,直接做还是超时,可以把询问离线,用树状数组维护
#include <bits/stdc++.h>
using namespace std;
const int N=100005;
int n, m,tot1,cnt,bk[N],fa[N],sh[N][30],bit[N],fail[N],tot,st[N],ed[N],ans[N],f[N];
char s[N];
struct node
{
int x,id;
node(int X=0,int I=0) : x(X) , id(I) {}
};vector<node> g[N];
struct edge
{
int v,next;
edge(int V=0,int N=0) : v(V) , next(N) {}
}e[2*N];
vector<int> ve[N];
void link(int u,int v)
{
ve[u].push_back(v);
ve[v].push_back(u);
}
void dfs(int u,int ff)
{
st[u]=++tot;
for(int i=0;i<ve[u].size();i++)
{
int v=ve[u][i];
if(v==ff)continue;
dfs(v,u);
}
ed[u]=tot;
return ;
}
queue<int>q;
void getfail()
{
for(int i=0;i<=25;i++)
{
if(sh[0][i])fail[sh[0][i]]=0,q.push(sh[0][i]); // 这里的0是根节点的编号;
}
while (!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<=25;i++)
{
if(!sh[u][i])sh[u][i]=sh[fail[u]][i];
else
{
q.push(sh[u][i]);
fail[sh[u][i]]=sh[fail[u]][i];
}
}
}
for(int i=1;i<=cnt;i++)
{
int u=i;
int v=fail[i];
link(u,v);
}
return ;
}
int lowbit(int x)
{
return x&(-x);
}
void modify(int x,int y)
{
for(;x<=tot;x+=lowbit(x))
bit[x]+=y;
return ;
}
int ask(int x)
{
int sum=0;
for(;x>0;x-=lowbit(x)) sum+=bit[x];
return sum;
}
int main()
{
scanf("%s",s+1);
int len=strlen(s+1),now=0;
for(int i=1;i<=len;i++)
{
if(s[i]>='a'&&s[i]<='z')
{
int c=s[i]-'a';
if(!sh[now][c])sh[now][c]=++cnt,fa[cnt]=now;
now=sh[now][c];
}
if(s[i]=='P')bk[++n]=now;
if(s[i]=='B')now=fa[now];
}
getfail();
dfs(0,0);
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
g[y].push_back(node(x,i));
}
now=0;
for(int i=1,j=0;i<=len;i++)
{
if(s[i]>='a'&&s[i]<='z')
{
int c=s[i]-'a';
now=sh[now][c];
modify(st[now],1);
}
if(s[i]=='P')
{
j++;
for(int k=0;k<g[j].size();k++)
{
int x=g[j][k].x;
ans[g[j][k].id]=ask(ed[bk[x]])-ask(st[bk[x]]-1);
}
}
if(s[i]=='B')
{
modify(st[now],-1);
now=fa[now];
}
}
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
return 0;
}