[NOI2011]阿狸的打字机(AC自动机+树状数组)

【题解】

KMP算法:每次询问时求出x串的失配函数,然后在y串上匹配,总复杂度O(m*len) 可以得40分 

如果把所有单词建成一棵字母树,考虑类似的暴力:
对每次询问,枚举y串的每个点(将这个点理解为x在y串上的最后一个匹配点),若从它沿失配指针到root的路径经过x串的最后一个点,则答案加1
由于(x1,y),(x2,y),…这些y相同的询问在AC自动机上走的路径一样,可以对于y一次全部求得,将询问统计后离线处理即可 

做到这一步后,程序的时间主要浪费在哪里呢?发现对于每个y串,都要处理它的每个字符到root的路径,而其实许多串都有很长的公共前缀,这里重复计算了 
可以反过来考虑:每个x串的最后一个点,沿失配指针逆向走,寻找所有y上的点 
这时如果把x看成一棵树的树根,把失配指针看成树边,那问题就转化为求以x为根的子树中有多少点在y串上 
因此,以失配指针为树边,将AC自动机建成一棵树(称fail树),只需要想办法把y上的所有点标在树上,并可以实现快速查询就行了 
这样的话,若y2与y1有公共前缀,标记y2的点时公共前缀就不用重新操作了 

快速查询子树中有多少标记点,可以联想到区间快速求和(树状数组)
而子树的确可以转化为一段连续的区间:
dfs整棵fail树,对每个结点记录进入、最后离开时间(L,R),则满足L<=L1<R1<=R的点(L1,R1)必在点(L,R)的子树中 
因此y串每标记一个点t,就是将fail树对应数列的L[t]位+1,删除同理,询问时查询( L[x_last] , R[x_last] )的区间和就行了 


【代码】

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<vector>
using namespace std;
vector<int> Ask[100005],Ans[100005],G[100005];//Ask[i]:所有y==i的询问对应的x 
char s[100005]={0};
int ch[100005][130]={0},ls[100005]={0},pre[100005]={0},L[100005]={0},R[100005]={0},c[200005]={0},f[100005]={0},q[100005]={0},x[100005]={0},y[100005]={0},p[100005]={0};
int len,u=0,sz=0,tot=0;
void tj(char t)
{
	if(ch[u][t]==0)
	{
		ch[u][t]=++sz;
		pre[ch[u][t]]=u;
	}
	u=ch[u][t];
}
void getf()
{
	int i,j,head=0,tail=0;
	for(i='a';i<='z';i++)
		if(ch[0][i]!=0)
		{
			q[tail++]=ch[0][i];
			G[0].push_back(ch[0][i]);
		}
	while(head<tail)
	{
		for(i='a';i<='z';i++)
			if(ch[q[head]][i]!=0)
			{
				q[tail++]=ch[q[head]][i];
				j=f[q[head]];
				while(j!=0&&ch[j][i]==0) j=f[j];
				f[ch[q[head]][i]]=ch[j][i];
				G[ch[j][i]].push_back(ch[q[head]][i]);
			}
		head++;
	}
}
void dfs(int x)
{
	int i;
	L[x]=++tot;
	for(i=0;i<G[x].size();i++)
		dfs(G[x][i]);
	R[x]=++tot;
}
void jia(int p,int x)//第p个位置的值加x
{
	for(;p<=2*len;p+=(p&(-p)))
		c[p]+=x;
}
int cx(int p)
{
	int sum=0;
	for(;p>0;p-=(p&(-p)))
		sum+=c[p];
	return sum;
}
int main()
{
	int m,i,j=0,k;
	scanf("%s",s);
	len=strlen(s);
	for(i=0;i<len;i++)
	{
		if(s[i]>='a'&&s[i]<='z') tj(s[i]);
		if(s[i]=='B') u=pre[u];
		if(s[i]=='P') ls[++j]=u;
	}
	scanf("%d",&m);
	for(i=1;i<=m;i++)
	{
		scanf("%d%d",&x[i],&y[i]);
		Ask[y[i]].push_back(x[i]);
	}
	getf();
	dfs(0);
	u=k=0;
	for(i=0;i<len;i++)
	{
		if(s[i]>='a'&&s[i]<='z')
		{
			u=ch[u][s[i]];
			jia(L[u],1);
		}
		if(s[i]=='B')
		{
			jia(L[u],-1);
			u=pre[u];
		}
		if(s[i]=='P')
		{
			k++;
			for(j=0;j<Ask[k].size();j++)
				Ans[k].push_back(cx(R[ ls[Ask[k][j]] ])-cx(L[ ls[Ask[k][j]] ]-1));
		}
	}
	for(i=1;i<=m;i++)
		printf("%d\n",Ans[y[i]][p[y[i]]++]);
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值