【NOI2011】阿狸的打字机

本文深入探讨使用AC自动机解决复杂字符串匹配问题的方法。通过构建Trie树和利用失败指针,文章详细讲解如何高效查询一个字符串在另一个字符串中出现的次数。采用离线查询和树状数组优化算法,实现快速准确的匹配计数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

luoguP2414

题目大意


  • 你需要支持下面几种操作
  • 在字符串尾部插入一个字符 
  • 在字符串尾部删除一个字符
  • 将字符串整个输出 
  • 询问第?次输出的字符串在第?次输出的字符串中出现了多少次
  • ?,? ≤ 10^5

分析


  • 首先你会发现他打字的方式非常奇妙,实际上不就是在构建一颗Tire吗?P相当于给节点标记,B相当于退回父亲节点,a..z相当于建立新的节点
  • 然后跑AC自动机得到fail指针
  • 首先得知道如何得出单个操作 x,y。AC自动机则架构在前缀树Tire上,自然地,判断AC自动机上面的两个子串x,y,x是否是y的子串,就等价于x是否是 y某个前缀s 的后缀!在AC自动机上,判断x是s的后缀非常简单,只要看s能否沿着fail指针走到x即可
  • 那么,查询x在y中出现了几次就比较明了了,只要看从根节点到y的路径中有多少个s,满足x是s的后缀即s能沿着fail指针走到x
  • 于是,我们可以将 fail[i] 作为 i 的父节点建立一颗新的树,这样的话如果 j 是 i 的祖先,那么 i 显然可以由fail指针走到 j。那么查询x,y时,将root->y的路径上的每一个点都变为1,那么答案就相当于x的子树中有多少个1了
  • 但是这样直接在线查询显然不行除非写一些神犇数据结构。单点修改子树查询可以用 dfs序+树状数组解决,因此我们可以离线,以y为关键字排序,这样就可以根据原来建立AC自动机的顺序进行修改了,只要在进入一个点t时+1,出去时-1,查询时自然root->y的路径上的每一个点都是1了

Code


#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
using namespace std;
const int N=1e5+100;

struct node{int y,n;}e[N];
struct ques{int x,y,id;}q[N];
int tr[N][30],ans[N],fa[N],pos[N],fail[N],c[N],l[N],r[N],lin[N],len=1,dfn=0,tot=0,cnt=0,n;
string s;

inline bool cmp(ques x,ques y){return x.y<y.y;}

void read(int x,int y)
{e[++len].y=y,e[len].n=lin[x],lin[x]=len;}

void dfs(int x){
	l[x]=++dfn;
	for(int i=lin[x];i;i=e[i].n)dfs(e[i].y);
	r[x]=dfn;
}

inline int lowbit(int x){return x&-x;}
void add(int x,int v){while(x<=dfn)c[x]+=v,x+=lowbit(x);}
int getsum(int x){int sum=0;while(x)sum+=c[x],x-=lowbit(x);return sum;}

void build(){
	int now=0,len=s.size();
	for(int i=0;i<len;i++){
		if(s[i]=='P') pos[++cnt]=now;
		else if(s[i]=='B') now=fa[now];
		else{
			int &pos=tr[now][s[i]-'a'];
			if(!pos)pos=++tot;
			fa[pos]=now,now=pos;
		}
	}
	queue<int> q;
	rep(i,0,25)if(tr[0][i])q.push(tr[0][i]);
	while(q.size()){
		int x=q.front();q.pop();
		rep(i,0,25){
			int &pos=tr[x][i];
			if(pos){
				fail[pos]=tr[fail[x]][i];
				q.push(pos);
			}else pos=tr[fail[x]][i];
		}
	}
}

void work(){
	int now=0,k=1,len=s.size();cnt=0;
	for(int i=0;i<len;i++){
		if(s[i]=='P'){
			cnt++;
			while(q[k].y==cnt&&k<=n){
				int tmp=pos[q[k].x];
				ans[q[k].id]=getsum(r[tmp])-getsum(l[tmp]-1);
				k++;
			}
		}else if(s[i]=='B'){
			add(l[now],-1);
			now=fa[now];
		}else{
			now=tr[now][s[i]-'a'];
			add(l[now],1);
		}
	}
}

int main()
{
	cin>>s>>n;
	rep(i,1,n){scanf("%d%d",&q[i].x,&q[i].y);q[i].id=i;}
	sort(q+1,q+n+1,cmp);
	build(); rep(i,1,tot)read(fail[i],i);
	dfs(0);
	work(); rep(i,1,n)printf("%d\n",ans[i]);
	return 0;
}

鸣谢


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值