刚学完字符串算法做一做题,这道题的质量的确很高,做完以后感觉对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]);
}
本文深入讲解AC自动机的实现思路及应用技巧,包括KMP算法、Trie树、Fail指针等关键概念,并通过一道典型题目展示如何运用AC自动机解决字符串匹配问题。
1594

被折叠的 条评论
为什么被折叠?



