P2414 [NOI2011] 阿狸的打字机
Description
阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有28个按键,分别印有26个小写英文字母和'B'、'P'两个字母。
经阿狸研究发现,这个打字机是这样工作的:
l 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
l 按一下印有'B'的按键,打字机凹槽中最后一个字母会消失。
l 按一下印有'P'的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。
例如,阿狸输入aPaPBbP,纸上被打印的字符如下:
a
aa
ab
我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数(x,y)(其中1≤x,y≤n),打字机会显示第x个打印的字符串在第y个打印的字符串中出现了多少次。
阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?
Input
输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。
第二行包含一个整数m,表示询问个数。
接下来m行描述所有由小键盘输入的询问。其中第i行包含两个整数x, y,表示第i个询问为(x, y)。
Output
输出m行,其中第i行包含一个整数,表示第i个询问的答案。
Sample Input
aPaPBbP
3
1 2
1 3
2 3
Sample Output
2
1
0
Hint
1<=N<=10^5
1<=M<=10^5
输入总长<=10^5
Source
思路:题目来源:P2414 [NOI2011]
不能
这打字机比现在的还先进,哪里老式了
阿 机 的 打 字 狸
提前说明,前面的几个算法都是会TLE就不写注释了,最后AC的代码有详细注释
这是一个模式串互相匹配的题,而且询问由很多,会有多个x对应一个y,也肯一个x多个y,那么就是一个多模式匹配的问题,如果直接对每个询问找答案直接暴毙,根本不用想,
想一下根据输入先建立字典树,然后对trie建立fail数组,题目让找每个y中有几个x,那么就是在trie树上在y链扫的时候,每走到一个节点就一直跳fail,如果跳到了x的末尾那么就答案+1,思路是可以,但是时间复杂度爆炸,交OJ只有40分,我没写这个代码
然后优化一下,想一下,每个节点只有一个对应的fail树,那么如果把fail的指向反转一下,不就是一对多,不就是个树了,那么以这个fail树上的某个x的末节点为根的子树,该字数的所有节点就是可以通过原来的fail数组跳到x末节点的可以+1的节点,所以,我们只要在扫y链的时候每第一次扫到y链某点,就这个点标记一下,一直到y链末尾,再计算对于所有的该y链的询问,具体就是对询问<x,y>,遍历以x末节点为根的fail树,看有多少个节点被标记(此时只有y链上的点打标了),那么该询问的答案就是标记数。我们知道一棵树欧拉序的进序(DFS序),根和字数的序号是连着的,所以就是一个区间查询的问题,那么怎么查询呢?x先暴力吧感觉会爆炸
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
char text [maxn];
int word[maxn],alphabet,wordp,trie[maxn][26],triecnt,tag[maxn],fail[maxn],failcnt,queue[maxn],front,rear;
int words[maxn],wordsid[maxn],wordsidcnt=0,wordscnt,eulerstart[maxn],eulerend[maxn],eulercnt,idx[maxn],seg[maxn];
int ans[maxn];
struct egde1
{
int tar,next,id;
}ques[maxn];
int queshead[maxn],quesegdecnt;
struct node2
{
int to,next;
}reverseedge[maxn];
int reversehead[maxn],reversecnt;
int insert(int *w,int len)
{
int u=0,c;
for(int i=0;i<len;i++)
{
c=w[i];
if(trie[u][c]==0)
trie[u][c]=++triecnt;
u=trie[u][c];
}
tag[u]=1;
return u;
}
void buildfail()
{
int u=0;
for(int i=0;i<26;i++)
{
if(trie[u][i])
queue[rear++]=trie[u][i];
}
while(front<rear)
{
u=queue[front++];
for(int i=0;i<26;i++)
{
int j=fail[u];
if(trie[u][i])
{
while(j!=0&&trie[j][i]==0)
j=fail[j];
fail[trie[u][i]]=trie[j][i];
queue[rear++]=trie[u][i];
}
}
}
}
void buildreverse()
{
for(int i=1;i<=triecnt;i++)
{
reverseedge[++reversecnt].next=reversehead[fail[i]];
reversehead[fail[i]]=reversecnt;
reverseedge[reversecnt].to=i;
}
}
void buildeuler(int u)
{
int v;
eulerstart[u]=++eulercnt;
idx[eulercnt]=u;
for(v=reversehead[u];v;v=reverseedge[v].next)
buildeuler(reverseedge[v].to);
eulerend[u]=eulercnt;
}
void dfstrie(int u)
{
seg[eulerstart[u]]++;
if(tag[u])
{
for(int v=queshead[u];v;v=ques[v].next)
{
int tot=0;
for(int i=eulerstart[ques[v].tar];i<=eulerend[ques[v].tar];i++)
{
tot+=seg[i];
}
ans[ques[v].id]=tot;
}
}
for(int i=0;i<26;i++)
{
if(trie[u][i])
{
dfstrie(trie[u][i]);
}
}
seg[eulerstart[u]]--;
}
int main()
{
scanf("%s",text);
int textlen=strlen(text);
for(int i=0;i<textlen;i++)
{
alphabet=text[i]-'a';
if(text[i]=='P')
{
wordsid[++wordsidcnt]=insert(word,wordp);
}
else if(text[i]=='B')
wordp--;
else
word[wordp++]=alphabet;
}
buildfail();
buildreverse();
buildeuler(0);
int q,x,y;
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&x,&y);
ques[++quesegdecnt].next=queshead[wordsid[y]];
ques[quesegdecnt].tar=wordsid[x];
ques[quesegdecnt].id=i;
queshead[wordsid[y]]=quesegdecnt;
}
dfstrie(0);
for(int i=1;i<=q;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
然后交OJ一看,其实还好上面这个有70分,蛮不错,但是还有三个点TLE
那么再优化一下,可以用线段树来维护区间修改查询,好的上代码
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
char text [maxn];
int word[maxn],alphabet,wordp,trie[maxn][26],triecnt,tag[maxn],fail[maxn],failcnt,queue[maxn],front,rear;
int words[maxn],wordsid[maxn],wordsidcnt=0,wordscnt,eulerstart[maxn],eulerend[maxn],eulercnt,idx[maxn],seg[maxn];
int ans[maxn];
struct node
{
int sum,l,r;
}segtree[4*maxn];
int segtreecnt;
struct egde1
{
int tar,next,id;
}ques[maxn];
int queshead[maxn],quesegdecnt;
struct node2
{
int to,next;
}reverseedge[maxn];
int reversehead[maxn],reversecnt;
int insert(int *w,int len)
{
int u=0,c;
for(int i=0;i<len;i++)
{
c=w[i];
if(trie[u][c]==0)
trie[u][c]=++triecnt;
u=trie[u][c];
}
tag[u]=1;
return u;
}
void buildfail()
{
int u=0;
for(int i=0;i<26;i++)
{
if(trie[u][i])
queue[rear++]=trie[u][i];
}
while(front<rear)
{
u=queue[front++];
for(int i=0;i<26;i++)
{
int j=fail[u];
if(trie[u][i])
{
while(j!=0&&trie[j][i]==0)
j=fail[j];
fail[trie[u][i]]=trie[j][i];
queue[rear++]=trie[u][i];
}
}
}
}
void buildreverse()
{
for(int i=1;i<=triecnt;i++)
{
reverseedge[++reversecnt].next=reversehead[fail[i]];
reversehead[fail[i]]=reversecnt;
reverseedge[reversecnt].to=i;
}
}
void buildeuler(int u)
{
int v;
eulerstart[u]=++eulercnt;
idx[eulercnt]=u;
for(v=reversehead[u];v;v=reverseedge[v].next)
buildeuler(reverseedge[v].to);
eulerend[u]=eulercnt;
}
void buildtree(int k,int l,int r)
{
segtree[k].l=l;
segtree[k].r=r;
if(l==r)
{
return;
}
int mid=(l+r)/2;
buildtree(2*k,l,mid);
buildtree(2*k+1,mid+1,r);
return;
}
void update(int k,int lo,int n)
{
if(segtree[k].l==segtree[k].r)
{
segtree[k].sum+=n;
return;
}
int mid=(segtree[k].l+segtree[k].r)/2;
if(lo<=mid)
update(2*k,lo,n);
else
update(2*k+1,lo,n);
segtree[k].sum=segtree[2*k].sum+segtree[2*k+1].sum;
}
int query(int k,int l,int r)
{
if(segtree[k].l==l&&segtree[k].r==r)
{
return segtree[k].sum;
}
int mid=(segtree[k].l+segtree[k].r)/2;
if(r<=mid)
return query(2*k,l,r);
else if(l>mid)
return query(2*k+1,l,r);
else
return query(2*k,l,mid)+query(2*k+1,mid+1,r);
}
void dfstrie(int u)
{
update(1,eulerstart[u],1);
if(tag[u])
{
for(int v=queshead[u];v;v=ques[v].next)
{
ans[ques[v].id]=query(1,eulerstart[ques[v].tar],eulerend[ques[v].tar]);
}
}
for(int i=0;i<26;i++)
{
if(trie[u][i])
{
dfstrie(trie[u][i]);
}
}
update(1,eulerstart[u],-1);
}
int main()
{
scanf("%s",text);
int textlen=strlen(text);
for(int i=0;i<textlen;i++)
{
alphabet=text[i]-'a';
if(text[i]=='P')
{
wordsid[++wordsidcnt]=insert(word,wordp);
}
else if(text[i]=='B')
wordp--;
else
word[wordp++]=alphabet;
}
buildfail();
buildreverse();
buildeuler(0);
int q,x,y;
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&x,&y);
ques[++quesegdecnt].next=queshead[wordsid[y]];
ques[quesegdecnt].tar=wordsid[x];
ques[quesegdecnt].id=i;
queshead[wordsid[y]]=quesegdecnt;
}
buildtree(1,1,eulercnt);
dfstrie(0);
for(int i=1;i<=q;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
那么把这个交上去一看还是70,该不过的点还是没过。。。
再优化,线段树和树状数组都是logn级别的但是树状数组常数比线段树小得多,那就树状数组试一下
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
char text [maxn];
int word[maxn],alphabet,wordp,trie[maxn][26],triecnt,tag[maxn],fail[maxn],failcnt,queue[maxn],front,rear;
int words[maxn],wordsid[maxn],wordsidcnt=0,wordscnt,eulerstart[maxn],eulerend[maxn],eulercnt,idx[maxn],seg[maxn],treearray[maxn];
int ans[maxn];
struct egde1
{
int tar,next,id;
}ques[maxn];
int queshead[maxn],quesegdecnt;
struct node2
{
int to,next;
}reverseedge[maxn];
int reversehead[maxn],reversecnt;
int insert(int *w,int len)
{
int u=0,c;
for(int i=0;i<len;i++)
{
c=w[i];
if(trie[u][c]==0)
trie[u][c]=++triecnt;
u=trie[u][c];
}
tag[u]=1;
return u;
}
void buildfail()
{
int u=0;
for(int i=0;i<26;i++)
{
if(trie[u][i])
queue[rear++]=trie[u][i];
}
while(front<rear)
{
u=queue[front++];
for(int i=0;i<26;i++)
{
int j=fail[u];
if(trie[u][i])
{
while(j!=0&&trie[j][i]==0)
j=fail[j];
fail[trie[u][i]]=trie[j][i];
queue[rear++]=trie[u][i];
}
}
}
}
void buildreverse()
{
for(int i=1;i<=triecnt;i++)
{
reverseedge[++reversecnt].next=reversehead[fail[i]];
reversehead[fail[i]]=reversecnt;
reverseedge[reversecnt].to=i;
}
}
void buildeuler(int u)
{
int v;
eulerstart[u]=++eulercnt;
idx[eulercnt]=u;
for(v=reversehead[u];v;v=reverseedge[v].next)
buildeuler(reverseedge[v].to);
eulerend[u]=eulercnt;
}
void update(int k,int n)
{
while(k<=eulercnt)
{
treearray[k]+=n;
k+=(k&(-k));
}
}
int sum(int k)
{
int ans=0;
while(k)
{
ans+=treearray[k];
k-=(k&(-k));
}
return ans;
}
void dfstrie(int u)
{
update(eulerstart[u],1);
if(tag[u])
{
for(int v=queshead[u];v;v=ques[v].next)
{
ans[ques[v].id]=sum(eulerend[ques[v].tar])-sum(eulerstart[ques[v].tar]-1);
}
}
for(int i=0;i<26;i++)
{
if(trie[u][i])
{
dfstrie(trie[u][i]);
}
}
update(eulerstart[u],-1);
}
int main()
{
scanf("%s",text);
int textlen=strlen(text);
for(int i=0;i<textlen;i++)
{
alphabet=text[i]-'a';
if(text[i]=='P')
{
wordsid[++wordsidcnt]=insert(word,wordp);
}
else if(text[i]=='B')
wordp--;
else
word[wordp++]=alphabet;
}
buildfail();
buildreverse();
buildeuler(0);
int q,x,y;
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&x,&y);
ques[++quesegdecnt].next=queshead[wordsid[y]];
ques[quesegdecnt].tar=wordsid[x];
ques[quesegdecnt].id=i;
queshead[wordsid[y]]=quesegdecnt;
}
dfstrie(0);
for(int i=1;i<=q;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
好的还是70真是棒呆了,那么到底卡在那了?其实是建立trie树的问题,会有很多大量重复字母的单词插入比如aaaaaaaaaaab和aaaaaaaaaaaaaaac这样建树的复杂度就暴毙了(其实这道题的描述里可以看出来会有大量这种数据),所以普通建树方法不能用了,那就直接在main里面用每次字母插入trie树,单词结束就打标,然后因为遇到'B'要删除一个字母,那么我们用个fa数组记录父亲方便回溯,至此AC代码就出来了,卡了我两天的题终于结束了
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
char text [maxn];//储存文本信息就是第一行的输入数据
int trie[maxn][26],beifen[maxn][26],triecnt,tag[maxn],fa[maxn],wordsid[maxn],wordsidcnt=0;//这是关于建立字典树的储存,trie数组是原始的字典树,beifen数组是tire的备份,在求fail的时候beifen被修改了
int fail[maxn],failcnt;
int queue[maxn],front,rear;
int eulerstart[maxn],eulerend[maxn],eulercnt,idx[maxn];//这个是存欧拉序的,eulerstart其实也是dfs序,主要是区间查询要用线段树或者树状数组维护
int seg[maxn],treearray[maxn];//seg是由欧拉序构成的区间,treearray是树状数组
int ans[maxn];
struct node1
{
int tar,next,id;
}ques[maxn];//问题是用图存的,具体就是链式前向星
int queshead[maxn],quesegdecnt;
struct node2
{
int to,next;
}reversenode[maxn];//这个是把faill数组的指向调头之后构成的faill树,这里命名为rewerse,因为和std库里重合了,就改成reversenode
int reversehead[maxn],reversecnt;
void buildfail()//这里是建立fail树,由于建立的时候逐个跳faill很慢,要用路径压缩就要就该trie树,那么我用备份来完成路径压缩,保证原来trie树不变
{//说一下路径压缩的时候,会对字典树修改,但是生产的faill数组与通过逐个跳faill指针算法的结果是一样的
int u=0;
for(int i=0;i<26;i++)
if(beifen[u][i])
queue[rear++]=beifen[u][i];
while(front<rear)
{
u=queue[front++];
for(int i=0;i<26;i++)
{
if(beifen[u][i])
{
fail[beifen[u][i]]=beifen[fail[u]][i];
queue[rear++]=beifen[u][i];//路径压缩
}
else
beifen[u][i]=beifen[fail[u]][i];//路径压缩
}
}
}
void buildreverse()//把faill指针反转,变成faill树,存入reversenode
{
for(int i=1;i<=triecnt;i++)
{
reversenode[++reversecnt].next=reversehead[fail[i]];
reversehead[fail[i]]=reversecnt;
reversenode[reversecnt].to=i;
}
}
void buildeuler(int u)//建立欧拉序,方便区间查询(从O(n)降到O(logn)),核心算法是DFS
{
int v;
eulerstart[u]=++eulercnt;
idx[eulercnt]=u;
for(v=reversehead[u];v;v=reversenode[v].next)
buildeuler(reversenode[v].to);
eulerend[u]=eulercnt;
}
void update(int k,int n)//这里的区间查询和修改用的是后缀数组
{
while(k<=eulercnt)
{
treearray[k]+=n;
k+=(k&(-k));//就是个lowbit
}
}
int sum(int k)//树状数组的性质导致区间只能查询1-k的和,就是前缀和
{
int ans=0;
while(k)
{
ans+=treearray[k];
k-=(k&(-k));
}
return ans;
}
void dfstrie(int u)
{
update(eulerstart[u],1);
if(tag[u])
{
for(int v=queshead[u];v;v=ques[v].next)
{
ans[ques[v].id]=sum(eulerend[ques[v].tar])-sum(eulerstart[ques[v].tar]-1);//由于sum查询的是前缀和,别忘了start的位置-1
}
}
for(int i=0;i<26;i++)
{
if(trie[u][i])
{
dfstrie(trie[u][i]);
}
}
update(eulerstart[u],-1);
}
int main()
{
scanf("%s",text);
int textlen=strlen(text);
int now=0;
for(int i=0;i<textlen;i++)//这里不是整个单词的插入,是一个一个字母插入,而且不是每次从根节点开始,如果整个单词插入会TLE
{
int c=text[i]-'a';
if(text[i]=='P')
{
tag[now]=1;
wordsid[++wordsidcnt]=now;
}
else if(text[i]=='B')
now=fa[now];
else
{
if(trie[now][c])
{
now=trie[now][c];
}
else
{
trie[now][c]=++triecnt;
beifen[now][c]=triecnt;
fa[triecnt]=now;
now=triecnt;
}
}
}
buildfail();
buildreverse();
buildeuler(0);
int q,x,y;
scanf("%d",&q);
for(int i=1;i<=q;i++)//离线问题,用链式前向星存
{
scanf("%d%d",&x,&y);
ques[++quesegdecnt].next=queshead[wordsid[y]];
ques[quesegdecnt].tar=wordsid[x];
ques[quesegdecnt].id=i;
queshead[wordsid[y]]=quesegdecnt;
}
dfstrie(0);
for(int i=1;i<=q;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
2021.8.10