[NOI2011]阿狸的打字机

[NOI2011]阿狸的打字机

这题可以大大加深我们对自动AC机的理解。

一上来:哇,这是什么神仙读入啊,如果按照它的要求一个一个把所有串建出来的话,肯定会MLE呀!

后来想一想,它这么读入,肯定构成一棵树,并且,它刚好是字典树!

这里是建树的方法。

void ins(){
	int x=1;
	for(int i=0;i<S;i++){
		if(s[i]=='P')t[x].fin.push_back(++n),pos[n]=x;
		else if(s[i]=='B'){
			if(x==1)continue;
			x=t[x].fa;
		}else{
			if(!t[x].ch[s[i]-'a'])t[x].ch[s[i]-'a']=++cnt,t[cnt].fa=x,t[x].org[s[i]-'a']=true;
			x=t[x].ch[s[i]-'a'];
		}
	}
}

注意到这里有一个将串映射到trie中节点的数组pos,以后有用。

然后就不会了,看了题解。

我们必须明确一点,就是一个节点的所有祖先fail内,包含了它的全部出现过的前缀。

因此,对于询问(x,y),我们找到x串的结尾节点,则该节点在fail树中的子树内,有多少个节点出现在了y串中,则询问(x,y)的答案就是多少。

因为对于每一个这样的节点,它总可以通过多次跳fail跳到x串的结尾节点上。

但是,就算这样,我们也无法非常便捷地求答案。

考虑转换思路,以y串为枚举对象,查询有多少节点在x串的结尾节点的子树内。

查询子树内信息,我们有经典结构dfs序

我们可以将询问离线下来,并将询问存储在y节点的位置上。然后在trie树上爆搜(注意是trie树,不是trie图,在求fail时连上的新点不能进入,不然会挂),每到一个节点就统计(以当前节点到根所表示的字符串)作为y的答案。

在dfs序上单点修改,区间求和,我们有树状数组

关键代码(爆炸警告):

for(int j=0;j<v[t[x].fin[i]].size();j++)
	res[v[t[x].fin[i]][j].second]=
    	ask(t[pos[v[t[x].fin[i]][j].first]].dfn+t[pos[v[t[x].fin[i]][j].first]].sz-1)-
        ask(t[pos[v[t[x].fin[i]][j].first]].dfn-1);

v是一个vector,存的是询问,其中firstx串,second是序号。

res是统计答案的数组。

ask是树状数组的前缀和。

dfn是dfs序。

szfail树中子树大小。

注意哪里是fail树,哪里是trie树,哪里是trie图!!

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,S,cnt=1,head[100100],lim,pos[100100],BIT[100100],res[100100];
char s[100100];
struct Edge{
	int to,next;
}edge[200100];
void ae(int u,int v){
	static int tot;
	edge[tot].next=head[u],edge[tot].to=v,head[u]=tot++;
}
void add(int x,int val){
	while(x<=lim)BIT[x]+=val,x+=x&-x;
}
int ask(int x){
	int sum=0;
	while(x)sum+=BIT[x],x-=x&-x;
	return sum;
}
struct AC_automaton{
	int ch[26],fa,fail,dfn,sz;
	bool org[26];
	vector<int>fin;
}t[100100];
void ins(){
	int x=1;
	for(int i=0;i<S;i++){
		if(s[i]=='P')t[x].fin.push_back(++n),pos[n]=x;
		else if(s[i]=='B'){
			if(x==1)continue;
			x=t[x].fa;
		}else{
			if(!t[x].ch[s[i]-'a'])t[x].ch[s[i]-'a']=++cnt,t[cnt].fa=x,t[x].org[s[i]-'a']=true;
			x=t[x].ch[s[i]-'a'];
		}
	}
}
vector<pair<int,int> >v[100100];
queue<int>q;
void build(){
	for(int i=0;i<26;i++){
		if(!t[1].ch[i])t[1].ch[i]=1;
		else t[t[1].ch[i]].fail=1,q.push(t[1].ch[i]),ae(1,t[1].ch[i]);
	}
	while(!q.empty()){
		int x=q.front();q.pop();
		for(int i=0;i<26;i++){
			if(t[x].ch[i])t[t[x].ch[i]].fail=t[t[x].fail].ch[i],q.push(t[x].ch[i]),ae(t[t[x].fail].ch[i],t[x].ch[i]);
			else t[x].ch[i]=t[t[x].fail].ch[i];
		}
	}
}
void getdfn(int x){
//	printf("%d\n",x);
	t[x].dfn=++lim,t[x].sz=1;
	for(int i=head[x];i!=-1;i=edge[i].next)getdfn(edge[i].to),t[x].sz+=t[edge[i].to].sz;
//	printf("%d\n",x);
}
void dfs(int x){
	add(t[x].dfn,1);
	for(int i=0;i<t[x].fin.size();i++){
//		printf("%d:%d\n",x,t[x].fin[i]);
		for(int j=0;j<v[t[x].fin[i]].size();j++)res[v[t[x].fin[i]][j].second]=ask(t[pos[v[t[x].fin[i]][j].first]].dfn+t[pos[v[t[x].fin[i]][j].first]].sz-1)-ask(t[pos[v[t[x].fin[i]][j].first]].dfn-1);
	}
	for(int i=0;i<26;i++)if(t[x].org[i])dfs(t[x].ch[i]);
	add(t[x].dfn,-1);
}
int main(){
	scanf("%s",s),S=strlen(s),memset(head,-1,sizeof(head)),ins(),build(),getdfn(1);
	scanf("%d",&m);
	for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[y].push_back(make_pair(x,i));
	dfs(1);
	for(int i=1;i<=m;i++)printf("%d\n",res[i]);
	return 0;
}
posted @   Troverld  阅读(114)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示