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\) 可以直接用

例题:

P3808

裸板子,暴力跳\(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;
}

P3796

与上一题相同,记录每个模式串的个数即可。

P5357

看似与前题相似,只需要处理相同字符串即可,交一发就发现会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);
	}
}

P2292

首先有一个显然的\(dp\)

\[dp[i]=dp[j] \& [i,j是否一个单词] \]

复杂度为\(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;
} 

P2414)

第一眼暴力,用打印的字符串建\(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;
}
posted @ 2024-08-15 15:35  storms11  阅读(11)  评论(2编辑  收藏  举报