2021.11.10 fail树

2023-05-13,

2021.11.10 fail

https://blog.csdn.net/niiick/article/details/87947160

1. AC自动机与fail树的神奇关系

1.1 AC自动机

匹配一个文本字符串S,从i开始,一步一步向fail[i]跳,直到根,每匹配一次最多转跳m个T(假设一共有m个T),时间复杂度最大是 \(O(m^2)\),建树的复杂度为 \(O(n)\)。

对于Luogu P5357https://www.luogu.com.cn/problquanzhiem/P5357,对于 \(1\le n \le 2 \times 10^5\),T的长度总和不超过 \(2 \times 10^5\),\(|S| \le 2 \times 10^5\)。

AC自动机肯定挂,没得说。

1.2 fail树

如果把fail指针反过来,变为从 fail[i]指向i,那么每次求T在S中出现了几次,只需要把S的所有节点标上1,求一下T最后的节点所在的子树的权值之和。如果乐意,还可以求个dfs序,然后用树状数组直接求出子树权值之和,修改和查询时间复杂度均为 \(O(\log n)\)。

2.练习题

https://www.luogu.com.cn/problem/P5357

1.1里出现的那道题。

题意:

如1.1。

分析:

如1.2。

代码如下:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std; const int N=2e6+10;
int n,tot,cnt[200010],ans,ru[N],fa[N];
char s[N],t[N];
struct node{
int son[26],fail,flag,ans;
void clear(){
memset(son,0,sizeof(son));
flag=fail=ans=0;
}
}trie[N];
queue<int>q; inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')w=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
s=s*10+ch-'0';
ch=getchar();
}
return s*w;
}
void insert(char *s,int idi){
int u=0,len=strlen(s);
//cout<<len<<endl;//
for(int i=1;s[i];i++){
if(trie[u].son[s[i]-'a']==0)trie[u].son[s[i]-'a']=++tot;//,
//cout<<"u "<<u<<" s[i]-'a' "<<s[i]-'a'<<" tot "<<tot<<endl;
u=trie[u].son[s[i]-'a'];
//cout<<u<<" ";
}
if(!trie[u].flag)trie[u].flag=idi;
fa[idi]=trie[u].flag;
//cout<<endl;
//cout<<idi<<" fa "<<fa[idi]<<" flag "<<trie[u].flag<<" u "<<u<<endl;
}
void build(){
for(int i=0;i<26;i++)if(trie[0].son[i])q.push(trie[0].son[i]);
while(!q.empty()){
int x=q.front();q.pop();
int faili=trie[x].fail;
for(int i=0;i<26;i++){
if(trie[x].son[i])
trie[trie[x].son[i]].fail=trie[trie[x].fail].son[i],
q.push((trie[x].son[i])),
++ru[trie[trie[x].son[i]].fail];
else trie[x].son[i]=trie[trie[x].fail].son[i];
}
}
}
void topu(){
for(int i=0;i<=tot;i++)if(!ru[i])q.push(i);
while(!q.empty()){
int x=q.front();q.pop();
cnt[trie[x].flag]=trie[x].ans;
int faili=trie[x].fail;
--ru[faili];
trie[faili].ans+=trie[x].ans;
if(!ru[faili])q.push(faili);
}
}
void query(char *t){
int u=0,len=strlen(t);
for(int i=1;t[i];i++)
u=trie[u].son[t[i]-'a'],++trie[u].ans;
} int main(){
//memset(&trie,0,sizeof(trie));
n=read();
for(int i=1;i<=n;i++)scanf("%s",s+1),insert(s,i);
build();
//for(int i=1;i<=n;i++)cout<<fa[i]<<" ";cout<<endl<<endl;
scanf("%s",t+1);
query(t);
topu();
for(int i=1;i<=n;i++)printf("%d\n",cnt[fa[i]]);
return 0;
}

https://www.luogu.com.cn/problem/P2414

题意:

给定一个字符串,按B最后一个字母消失,按P打印之前的字母并换行。给定 \((x,y)\) 求x在y中出现多少次。

分析:

\(1 \le n \le 10^5\),\(1 \le m \le 10^5\),用自动机肯定会超时。如果我们先把y拍拍序,对于同一个y的二元组一起查询会比单个按原来顺序查询快很多,参考1.2。

代码如下:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std; const int N=1e5+10;
int m,cnt,ind,dfsx[N],sizei[N],head[N],ans[N];
char s[N];
struct queryi{
int x,y,id;
bool operator <(const queryi &b)const{
return y==b.y?x<b.x:y<b.y;
}
}query[N];
struct node{
int to,next;
}a[N]; inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')w=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
s=s*10+ch-'0';
ch=getchar();
}
return s*w;
}
inline void add(int u,int v){
++cnt;
a[cnt].to=v;
a[cnt].next=head[u];
head[u]=cnt;
}
inline void dfs(int x){
dfsx[x]=++ind;
sizei[x]=1;
for(int i=head[x];i;i=a[i].next){
int v=a[i].to;
dfs(v);
sizei[x]+=sizei[v];
}
}
struct AC{
int t[N][26],id[N],fail[N],tot=0,top=0,fa[N];
queue<int>q;
void insert(char *s){
int len=strlen(s),u=0;
for(int i=0;i<len;i++){
int v=s[i]-'a';
if(s[i]=='B')u=fa[u];
else if(s[i]=='P')id[++top]=u;
else{
if(!t[u][v])t[u][v]=++tot,fa[tot]=u;
u=t[u][v];
}
}
}
void build(){
for(int i=0;i<26;i++)if(t[0][i])q.push(t[0][i]);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(t[u][i]){
fail[t[u][i]]=t[fail[u]][i];
q.push(t[u][i]);//,add(fail[t[u][i]],t[u][i]);
}else t[u][i]=t[fail[u]][i];
}
}
for(int i=1;i<=tot;i++)add(fail[i],i);
}
}trie;
struct BIT{
int t[N*5];
int lowbit(int x){
return x&-x;
}
void add(int x,int k){
for(int i=x;i<=ind;i+=lowbit(i))t[i]+=k;
}
int query(int x){
int fin=0;
for(int i=x;i>0;i-=lowbit(i))fin+=t[i];
return fin;
}
void clear(){
memset(t,0,sizeof(t));
}
}tree;
inline void solve(char *s){
int len=strlen(s),u=0,js=0,now=1;
for(int i=0;i<len;i++){
//cout<<"i "<<i<<" u "<<u<<endl;
int v=s[i]-'a';
if(s[i]=='B')tree.add(dfsx[u],-1),u=trie.fa[u];
else if(s[i]=='P'){
++js;
while(query[now].y==js){
int idi=query[now].id,x=trie.id[query[now].x];
ans[idi]+=tree.query(dfsx[x]+sizei[x]-1)-tree.query(dfsx[x]-1);
++now;
}
}else u=trie.t[u][v],tree.add(dfsx[u],1);
}
} int main(){
scanf("%s",s);
int len=strlen(s);
trie.insert(s);
//cout<<"Case 1"<<endl;
trie.build();
dfs(0);
m=read();
for(int i=1;i<=m;i++)query[i].x=read(),query[i].y=read(),query[i].id=i;
sort(query+1,query+m+1);
//for(int i=1;i<=m;i++)cout<<"id "<<query[i].id<<" x "<<query[i].x<<" y "<<query[i].y<<endl;
solve(s);
for(int i=1;i<=m;i++)cout<<ans[i]<<endl;
return 0;
}

https://www.luogu.com.cn/problem/P3966

题意:

小张最近在忙毕设,所以一直在读论文。一篇论文是由许多单词组成但小张发现一个单词会在论文中出现很多次,他想知道每个单词分别在论文中出现了多少次。

分析:

直接给每个字符串加入trie时所经过的每个节点加1,求出每个字符串最后节点的fail树的权值之和。

代码如下:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std; const int N=2e6+10;
int n,trie[N][26],cnt,end[N],fail[N],size[N],q[N];
char s[N]; inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')w=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
s=s*10+ch-'0';
ch=getchar();
}
return s*w;
} void insert(char *s,int id){
int u=0;
for(int i=1;s[i];i++){
int v=s[i]-'a';
if(!trie[u][v])trie[u][v]=++cnt;
u=trie[u][v];
++size[u];
}
end[id]=u;
}
void build(){
//queue<int>q;
int head=0,tail=0;
for(int i=0;i<26;i++)if(trie[0][i])q[++tail]=trie[0][i];
while(head<tail){
int u=q[++head];
for(int i=0;i<26;i++){
int v=trie[u][i],faili=fail[u];
if(v)fail[v]=trie[faili][i],q[++tail]=v;
else trie[u][i]=trie[faili][i];
}
}
}
void solve(){
for(int i=cnt;i>=0;i--)size[fail[q[i]]]+=size[q[i]];
for(int i=1;i<=n;i++)cout<<size[end[i]]<<endl;
} int main(){
n=read();
for(int i=1;i<=n;i++)scanf("%s",s+1),insert(s,i);
build();
solve();
return 0;
}

2021.11.10 fail树的相关教程结束。

《2021.11.10 fail树.doc》

下载本文的Word格式文档,以方便收藏与打印。