这一题是对AC自动机的充分理解和树dfs序的巧妙运用。

题目背景

阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。

题目描述

打字机上只有28个按键,分别印有26个小写英文字母和'B'、'P'两个字母。经阿狸研究发现,这个打字机是这样工作的:

  • 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
  • 按一下印有'B'的按键,打字机凹槽中最后一个字母会消失。
  • 按一下印有'P'的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。

例如,阿狸输入aPaPBbP,纸上被打印的字符如下:

a aa ab 我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数\((x,y)\)(其中\(1≤x,y≤n\)),打字机会显示第\(x\)个打印的字符串在第\(y\)个打印的字符串中出现了多少次。

阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?

输入输出格式

输入格式:

输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。

第二行包含一个整数\(m\),表示询问个数。

接下来\(m\)行描述所有由小键盘输入的询问。其中第\(i\)行包含两个整数\(x,y\),表示第\(i\)个询问为\((x,y)\)。

输出格式:

输出\(m\)行,其中第\(i\)行包含一个整数,表示第\(i\)个询问的答案。

输入输出样例

输入样例#1:

aPaPBbP
3
1 2
1 3
2 3
输出样例#1:

2
1
0

说明

题解:

    AC自动机学习笔记中,我提到了fail树的每一条链上的点代表的字母都是一样的。实际上,根据fail和nxt的定义,在这条链上深度较深的点总包含深度较浅的点。更确切地说,深度较浅的点总是深度较深的点的后缀。

    有了上面的结论,我们就有了一种朴素算法:建立AC自动机,读取询问时让主串向父亲跳来检验fail指针指向的节点是不是模式串的结尾。接着沿fail指针检验一趟看是不是模式串的结尾,比KMP还要暴力一点。换句话说,就是个用AC自动机实现的KMP,而且代码巨短

Code 40pts:

#include<cstdio>
#include<cstring>
struct node
{
node *fa,*ch[26];
node *fail;
node(node *fa)
{
this->fa=fa;
memset(ch,0,sizeof(ch));
fail=NULL;
}
node()
{
fa=NULL;
fail=NULL;
memset(ch,0,sizeof(ch));
}
}*root=new node(),*End[100100];
char s[100100];
int cnt=0;
node *q[100100];
int l=0,r=0;
void Fail()
{
root->fail=root;
for(int i=0;i<26;++i)
if(root->ch[i])
{
root->ch[i]->fail=root;
q[++r]=root->ch[i];
}
else
root->ch[i]=root;
while(l<r)
{
node *p=q[++l];
for(int i=0;i<26;++i)
if(p->ch[i])
{
p->ch[i]->fail=p->fail->ch[i];
q[++r]=p->ch[i];
}
else
p->ch[i]=p->fail->ch[i];
}
return;
}
int main()
{
scanf("%s",s);
int l=strlen(s);
node *now=root;
for(int i=0;i<l;++i)
if(s[i]=='P')
End[++cnt]=now;
else if(s[i]=='B')
now=now->fa;
else
{
if(!now->ch[s[i]-'a'])
now->ch[s[i]-'a']=new node(now);
now=now->ch[s[i]-'a'];
}
Fail();
int u,v,n;
scanf("%d",&n);
while(n--)
{
scanf("%d%d",&u,&v);
node *now=End[v];
int sum=0;
while(now!=root)
{
node *p=now;
while(p!=root)
{
if(p==End[u])
++sum;
p=p->fail;
}
now=now->fa;
}
printf("%d\n",sum);
}
return 0;
}

    发现上面这种做法需要递归fail指针,我们可以考虑构建fail树,设主串的结束节点为\(a\),模式串的结束节点为\(b\),那么看fail树中\(b\)的子树中有多少个节点在主串上。(注意这里不是主串结尾,在主串中间也可以)因为子树上的点通过fail边总可以连接到\(b\)这个节点上来,因此整棵子树都可以对\(b\)做出贡献。可以稍微优化一点时间复杂度,但是得分还是在40到50左右。

    在字典树上,有一种肥肠玄妙的优化叫树状数组。我们在这个题中只需要对字典树进行一遍DFS就可以了,在DFS过程中就可以处理各种询问,有点像tarjan求LCA算法的步骤。

    首先我们明确fail树和字典树上的节点是一一对应的。但是fail树需要一个dfs序,这里不要和节点编号或者节点指针搞混了;先对fail树进行dfs,存下每个点的编号(dfn)和子树大小(sz),方便之后对子树进行统计。

    接着对字典树进行dfs,遇到一个节点就把这个节点的dfn在树状数组中+1,表明这个节点在当前访问的串中。当访问到一个作为结束点的节点时,就处理它作为主串的询问。此时有了上面一点点优化(可达50分)的思想,直接查询每个询问的模式串结束节点的子树中有多少个被+1了,因为是子树,在dfs序上是连续的,所以树状数组求区间和就可以了。回溯时不要忘了把这个点的dfn在树状数组中-1,表示不属于接下来访问的串。

    这个题写起来真爽——虽然调的也很爽

Code:

#include<cstdio>
#include<cstring>
#include<vector>
using std::vector;
struct node//AC自动机
{
node *ch[26],*fail,*fa;
vector<int> End;//是哪些字符串的结束点
int sz,num;//dfs序用的子树大小和自己的编号(dfn)
int head,ahead;
bool exi[26];
node(node *fa)
{
memset(exi,0,sizeof(exi));
this->fa=fa;
head=-1;
ahead=-1;
fail=NULL;
memset(ch,0,sizeof(ch));
}
node()
{
memset(exi,0,sizeof(exi));
head=-1;
ahead=-1;
fa=NULL;
fail=NULL;
memset(ch,0,sizeof(ch));
}
}*root=new node(),*End[100100];
//存fail树的边
struct edge
{
node *n;
int nxt,num;
edge(node *n,int nxt,int num)
{
this->n=n;
this->nxt=nxt;
this->num=num;
}
edge(node *n,int nxt)
{
this->n=n;
this->nxt=nxt;
}
edge(){}
}e[201000],ask[101000];
int ecnt=-1,acnt=-1;
void add(node *from,node *to)
{
e[++ecnt]=edge(to,from->head);
from->head=ecnt;
e[++ecnt]=edge(from,to->head);
to->head=ecnt;
}
void Add(node *from,node *to,int num)
{
ask[++acnt]=edge(to,from->ahead,num);
from->ahead=acnt;
}
//树状数组
int c[100100];
int lowbit(int x)
{
return x&(-x);
}
int dcnt=0;
void change(int x,int v)
{
while(x<=dcnt)
{
c[x]+=v;
x+=lowbit(x);
}
return;
}
int sum(int x)
{
int ans=0;
while(x)
{
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
//求Fail
node *q[100100];
int l=0,r=0;
void Fail()
{
root->fail=root;
for(int i=0;i<26;++i)
if(root->ch[i])
{
root->ch[i]->fail=root;
add(root,root->ch[i]);
q[++r]=root->ch[i];
}
else
root->ch[i]=root;
while(l<r)
{
node *p=q[++l];
for(int i=0;i<26;++i)
if(p->ch[i])
{
p->ch[i]->fail=p->fail->ch[i];
add(p->ch[i],p->fail->ch[i]);
q[++r]=p->ch[i];
}
else
p->ch[i]=p->fail->ch[i];
}
return;
}
//dfs求dfs序
void dfs(node *x,node *from)
{
x->sz=1;
x->num=++dcnt;
for(int i=x->head;~i;i=e[i].nxt)
if(e[i].n!=from)
{
dfs(e[i].n,x);
x->sz+=e[i].n->sz;
}
return;
}
//dfs求答案
int ans[100100];
void Dfs(node *x)
{
change(x->num,1);
for(int i=x->ahead;~i;i=ask[i].nxt)
{
node *p=ask[i].n;
ans[ask[i].num]=sum(p->num+p->sz-1)-sum(p->num-1);
}
for(int i=0;i<26;++i)
if(x->exi[i])
Dfs(x->ch[i]);
change(x->num,-1);
}
char s[101000];
int main()
{
scanf("%s",s);
node *now=root;
int cnt=0;
for(int i=0;s[i]!='\0';++i)
if(s[i]=='P')
{
now->End.push_back(++cnt);
End[cnt]=now;
}
else if(s[i]=='B')
now=now->fa;
else
{
if(!now->ch[s[i]-'a'])
now->ch[s[i]-'a']=new node(now);
now->exi[s[i]-'a']=1;
now=now->ch[s[i]-'a'];
}
Fail();
dfs(root,root);
int n,u,v;
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
scanf("%d%d",&u,&v);
Add(End[v],End[u],i);
}
Dfs(root);
for(int i=1;i<=n;++i)
printf("%d\n",ans[i]);
return 0;
}

  

【AC自动机】【树状数组】【dfs序】洛谷 P2414 [NOI2011]阿狸的打字机 题解的更多相关文章

  1. 【BZOJ】2434: [Noi2011]阿狸的打字机 AC自动机+树状数组+DFS序

    [题意]阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机.打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P'两个字母. 经阿狸研究发现,这个打字机是这样工作的: l 输入小写 ...

  2. BZOJ.2434.[NOI2011]阿狸的打字机(AC自动机 树状数组 DFS序)

    题目链接 首先不需要存储每个字符串,可以将所有输入的字符依次存进Trie树,对于每个'P',记录该串结束的位置在哪,以及当前节点对应的是第几个串(当前串即根节点到当前节点):对于'B',只需向上跳一个 ...

  3. 洛谷 P2414 [NOI2011]阿狸的打字机 解题报告

    P2414 [NOI2011]阿狸的打字机 题目背景 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机. 题目描述 打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P'两个字母 ...

  4. BZOJ 2780 Sevenk Love Oimaster (后缀自动机+树状数组+dfs序+离线)

    题目大意: 给你$n$个大串和$m$个询问,每次给出一个字符串$s$询问在多少个大串中出现过 好神的一道题 对$n$个大串建出广义$SAM$,建出$parent$树 把字符串$s$放到$SAM$里跑, ...

  5. luogu SP8093 后缀自动机+树状数组+dfs序

    这题解法很多,简单说几个: 1. 线段树合并,时间复杂度是 $O(nlog^2n)$ 的. 2. 暴力跳 $fail,$ 时间复杂度 $O(n\sqrt n),$ 比较暴力. 3. 建立后缀树后在 $ ...

  6. 洛谷P2414 [NOI2011]阿狸的打字机(AC自动机)

    传送门 考虑一下,如果串B在串A中出现过,那么A的fail指针必定直接或间接指向B 那么我们可以把fail树建起来,那么就变成B代表的节点的子树里有多少节点属于A 然后这就是一个序列统计问题,直接用d ...

  7. 洛谷P2414 - [NOI2011]阿狸的打字机

    Portal Description 首先给出一个只包含小写字母和'B'.'P'的操作序列\(s_0(|s_0|\leq10^5)\).初始时我们有一个空串\(t\),依次按\(s_0\)的每一位进行 ...

  8. 洛谷P2414 阿狸的打字机 [NOI2011] AC自动机+树状数组/线段树

    正解:AC自动机+树状数组/线段树 解题报告: 传送门! 这道题,首先想到暴力思路还是不难的,首先看到y有那么多个,菜鸡如我还不怎么会可持久化之类的,那就直接排个序什么的然后按顺序做就好,这样听说有7 ...

  9. Codeforces 587F - Duff is Mad(根号分治+AC 自动机+树状数组)

    题面传送门 第一眼看成了 CF547E-- 话说 CF587F 和 CF547E 出题人一样欸--还有另一道 AC 自动机的题 CF696D 也是这位名叫 PrinceOfPersia 的出题人出的- ...

随机推荐

  1. maven安装错误履历

    1\:maven cannot find entry:"/src/main/java" 先删除source下的文件夹 再新建文件夹

  2. shell 别名alias

    在这说下 shell   命令 alias 别名   看个人爱好 设置. 直接执行命令  显示当前所有别名 alias  别名='新的别名'  该命令在当窗口关闭以后 会失效  想要永久生效  需要在 ...

  3. SqlServer-truncate && delete && drop 的区别

    有些人在删除表的所有记录的时候,喜欢这样来——不给DELETE 语句提供WHERE 子句,表中的所有记录都将被删除.但这种方法是不可取的,正确的应该使用 TRUNCATE TABLE tb_name ...

  4. ubuntu 设置虚拟机和主机在同一网段

    一.在VMware中将网络连接方式设置为桥接 1.打开VM菜单栏->Settings 2.在弹出的Virtual Machine Setting对话框中点击Network Adapter,在右边 ...

  5. Apache ab命令

    一.简介 ab是apache自带的压力测试工具.ab非常实用,它不仅可以对apache服务器进行网站访问压力测试,也可以对或其它类型的服务器进行压力测试.比如nginx.tomcat.IIS等. 二. ...

  6. 几种导入osm(openstreetmap)数据的方法

    一osm2pgsql+postgresql+postgis osm2pgsql­——是由OpenStreetMap开发的一个命令行工具负责将OSM数据导入到基于PostgresSql的Postgis的 ...

  7. 事物处理service层的方法

    package cn.lijun.service; import java.sql.Connection;import java.sql.SQLException; import cn.lijun.d ...

  8. The Difference Between @Helpers and @Functions In WebMatrix

    from: http://www.mikesdotnetting.com/article/173/the-difference-between-helpers-and-functions-in-web ...

  9. HackThirteen 在onCreate()方法中获取View的宽度和高度

    1.概要:     Android源代码中很多模块都使用了post()方法,深入理解框架曾运行机制对于避开类似于本例中的小陷阱是很重要的 2.问题提出:     如果开发一些依赖于UI控件的宽和高的功 ...

  10. WinForm中的多语言处理

    配置文件中存储当前语言环境,切换语言时进行修改,启动程序时读取该配置并设置当前线程的Culture 可根据线程的语言环境动态读取不同的资源文件,不同资源文件名用语言环境文本进行区分