NOI2011阿狸的打字机

本文深入讲解AC自动机的实现思路及应用技巧,包括KMP算法、Trie树、Fail指针等关键概念,并通过一道典型题目展示如何运用AC自动机解决字符串匹配问题。

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

刚学完字符串算法做一做题,这道题的质量的确很高,做完以后感觉对AC自动机有长进

一下的神仙思路来自yyb dalao%%%,蒟蒻开始只想到了40分暴力,全程靠题解

STEP1

首先直接处理出所有的串再裸KMP好写,但是觉得得分应该不高,也没有人说能拿多少分

这个题正解的第一步是要想到AC自动机,准确地说和Fail指针关系密切

首先明确一个定理,如果当前沿着目标串A在trie树下向下走,走到了一个节点,发现这个节点指向B串的末尾,说明A串一定包含B串

所以一个朴素的想法已经出来了,A串在Trie树上的每个节点都沿失配指针一直跳,如果能跳到B串的末尾,这个节点就是合格的。所有合格节点的数目就是B串在A串上的出现次数(所有沿着失配指针跳到的节点都是B串末尾)。trie树中的每个节点可以记录在trie树中的fa,在记录一下每个串末尾节点编号。所以查询每个y串的时候,沿着它的末尾节点一路跳到根就可以了。

STEP2

可以发现每次跳fail指针只为找一种X串末尾实在太浪费了,所以可以把所有询问按Y排序,所有Y相同的询问一并处理,Y串Tire树中的每一个节点沿着失配指针跳到的所有节点,如果作为某串结尾,就用桶统计一下这个串被访问了多少次。这样会提高一部分效率,然而良心出题人也把这个聪明的操作提高了三十分,现在可以拿到70分

STEP3

可以发现每一个节点只有一个fail指针,所以这就满足树的条件,可以把fail指针取反成边,构造一颗fail树。所以问题由y上的每个节点能有几个沿fail指针跳到x串末尾,变为了x串末尾能跳到几个y串节点。可以发现,x串末尾节点能跳到的所有的y串上的节点,在fail树中都是它的子节点,所以问题就相当于x串末尾节点的fail树子树中有几个是y串上的点。可以发现一个节点,它的子树dfs序是连续的,所以就可以用数据结构维护一下(我用的常数较小的树状数组),y串遍历到的每一个节点插入到数据结构中+1,查询时统计一下区间和。这个做法还是70分

STEP4

这就是最神仙操作的正解了。我们发现每次把y串挨个节点插入到数据结构中实在是有点浪费,所以就会想到有没有高效一些的操作。这里给出我在luogu上看到的两种做法

1

首先是yyb dalao的,yyb大佬是把trie树上每个节点多打了一个标记,记录这个节点是哪个串的末尾。然后把整个trie树遍历一遍,因为tire树满足一个性质,就是如果能走到一个串的末尾节点,那么当前从根走过来的路径就是这个串,不多字符不少字符。

所以就把这个trie树的每个节点都遍历一遍,遍历到它时把这个节点的dfs序在数据结构中对应的位置+1,退出前再-1

中间判断一下这个节点是不是一个y串的末尾,如果是,就在此时求对应x串结尾的子树和


这个方法对trie树掌握的已经相当神了,我第一遍看完以后简直跪了,然而我发现这样的话会被重复串hack掉,比如第二个串和第四个串一样,这样第二个串末尾节点标记就会被第四个节点冲掉,这样就会认为它只是第四个串的末尾,第二个串是不存在的,所以如果有和第二个串有关的查询,答案就会变成0

2

这个做法是守望dalao的代码。为了避免刚才那种情况,我们就模拟一下这个打字机,统计当前串的序号。每当遇到B退格时,上一个字符对应节点dfs序在数据结构中-1。每当遇到新的小写字符,数据结构中该点dfs序对应位置+1。

当遇到P时,说明到了一个串的末尾。那么当前生成过几个串的数目+1,就是这个串的编号。所幸我们把询问按y排序,所以序号靠前的串早处理。具体操作就是碰到p时,如果当前生成的序号编号为Num,那么对于每个Num==y的询问,都处理出x的子树和

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define MAXN (200010)
using namespace std;
int L,Q_num,Size,fa[MAXN],s[MAXN][30],f[MAXN],end[MAXN],n;
int dfn[MAXN],low[MAXN],h[MAXN],m1,dfs_clock,ans[MAXN];
struct edge{
	int next,to,id;
	void Add(int Next,int To){
		next=Next; to=To;
	}
}q[MAXN*4];
void addedge(int x,int y){
	q[++m1].Add(h[x],y); h[x]=m1;
}
struct Treearray{
	int c[MAXN];
	int lowbit(int x){
		return x&(-x);
	}
	void change(int pos,int x){
		while (pos<=Size+1){
			c[pos]+=x;
			pos+=lowbit(pos);
		}
	}
	int Sum(int pos){
		int res=0;
		while (pos>0){
			res+=c[pos];
			pos-=lowbit(pos);
		}
		return res;
	}
	int query(int l,int r){
		return Sum(r)-Sum(l-1);
	}
}Ta;
struct Qu{
	int x,y,id;
	bool operator< (const Qu &a)const{
		return y<a.y;
	} 
	void Read(){
		scanf("%d %d",&x,&y);
	}
}qu[MAXN];
char St[MAXN];
void Get_fail(){
	queue <int> Q;
	int i,y,x;
	for (i=0;i<26;i++)
		if (s[0][i])
			Q.push(s[0][i]);
	while (!Q.empty()){
		x=Q.front(); Q.pop();
		for (i=0;i<26;i++){
			y=s[x][i];
			if (!y) s[x][i]=s[f[x]][i];
			else{
				int v=f[x];
				while (v&&!s[v][i]) v=f[v];
				f[y]=s[v][i];
				Q.push(y);
			}
		}
	}
}
void Build(){
	int d,rt=0,i;
	for (i=0;i<L;i++){
		if (St[i]=='B') rt=fa[rt];
		if (St[i]=='P')	end[++n]=rt;
		if (St[i]>='a'&&St[i]<='z'){
			d=St[i]-'a';
			if (!s[rt][d]) s[rt][d]=++Size,fa[Size]=rt;
			rt=s[rt][d];
		}
	}
}
void Dfs(int x){
	int i,y;
	dfn[x]=++dfs_clock;
	for (i=h[x];i;i=q[i].next){
		y=q[i].to;
		Dfs(y);
	}
	low[x]=dfs_clock;
}
void Work(){
	int i,rt=0,p=0,tot=1,x,d;
	for (i=0;i<L;i++){
		if (St[i]=='B') {
			Ta.change(dfn[rt],-1);
			rt=fa[rt];
			continue;
		}
		if (St[i]=='P'){
			p++;
			while (p==qu[tot].y){
				x=qu[tot].x;
				ans[qu[tot].id]=Ta.query(dfn[end[x]],low[end[x]]);
				tot++;
			}
			continue;
		}
		d=St[i]-'a';
		rt=s[rt][d]; Ta.change(dfn[rt],1);
	}
}
int main(){
	scanf("%s",St);
	L=strlen(St);
	scanf("%d",&Q_num);
	int i;
	for (i=1;i<=Q_num;i++) qu[i].Read(),qu[i].id=i;
	sort(qu+1,qu+Q_num+1);
	Build();
	Get_fail();
	for (i=0;i<=Size;i++) if (f[i]!=i) addedge(f[i],i);
	Dfs(0);
	Work();
	for (i=1;i<=Q_num;i++) printf("%d\n",ans[i]);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值