题目描述
阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有 (28) 个按键,分别印有 (26) 个小写英文字母和 B
、P
两个字母。经阿狸研究发现,这个打字机是这样工作的:
输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
按一下印有 B
的按键,打字机凹槽中最后一个字母会消失。
按一下印有 P
的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。
例如,阿狸输入 aPaPBbP
,纸上被打印的字符如下:
a
aa
ab
我们把纸上打印出来的字符串从 (1) 开始顺序编号,一直到 (n)。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数 ((x,y))(其中 (1leq x,yleq n)),打字机会显示第 (x) 个打印的字符串在第 (y) 个打印的字符串中出现了多少次。
阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?
输入格式
输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。
第二行包含一个整数 (m),表示询问个数。
接下来 (m) 行描述所有由小键盘输入的询问。其中第 (i) 行包含两个整数 (x, y),表示第 (i) 个询问为 ((x, y))。
输出格式
输出 (m) 行,其中第 (i) 行包含一个整数,表示第 (i) 个询问的答案。
输入样例
aPaPBbP
3
1 2
1 3
2 3
输出样例
2
1
0
数据范围
对于 (100\%) 的数据,(1leq nleq 10^5) ,(1leq mleq 10^5),第一行总长度 (leq 10^5)。
题解
首先我们可以 (O(|s|)) 把给出的字符串 (s) 丢到 trie树上,然后建出AC自动机的(fail)指针。由(fail)指针的性质,如果 (fail[u]=v) ,则说明从trie树的根节点到(v)的这个字符串是从trie树的根节点到(u)的这个字符串的子串。去判断一个字符串是否是另一个字符串的子串可以转换为判断一个字符串是否是另一个字符串的前缀的后缀。于是对于询问 ((x,y)),从trie树的根节点到 (y) 的这条链上有多少个点的(fail) 指针直接或间接地指向 (x),即为 (y) 中有多少个子串等于 (x)。那么显然能通过 (fail) 指针直接或间接地指向 (x) 的点都在 (fail) 中以 (x) 为根的子树中,于是我们根据 (fail) 树的DFS序建树状数组。我们把所有 (y) 相同的询问合并,然后去 DFS trie树,每DFS到一个点,就在树状数组上的对应点加1,每退出一个点就减1,这样若遇到一个 (y) 点,可以保证从trie树的根节点到 (y) 的这条链上的点权值都为1,然后去遍历 (y) 点为当前点时所有询问的 (x),在树状数组中求 (fail) 树中以 (x) 为根的子树的点权和即可。时间复杂度 (O(|s|+(|s|+m)log |s|))。
Code
#include <bits/stdc++.h>
using namespace std;
#define RG register int
#define LL long long
const int maxn=100010;
template<typename elemType>
inline void Read(elemType &T){
elemType X=0,w=0; char ch=0;
while(!isdigit(ch)) {w|=ch=='-';ch=getchar();}
while(isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar();
T=(w?-X:X);
}
inline int lowbit(int x){return x&(-x);}
struct BIT{
int Node[maxn];
int N;
void set_Len(int len){N=len;}
void Update(int x,int Add){for(;x<=N;x+=lowbit(x)) Node[x]+=Add;}
int PrefixSum(int x){int Res=0;for(;x;x-=lowbit(x)) Res+=Node[x];return Res;}
int Query(int L,int R){return PrefixSum(R)-PrefixSum(L-1);}
};
struct Graph{
struct edge{int Next,to;};
edge G[maxn<<1];
int head[maxn];
int cnt;
Graph():cnt(2){}
void clear(int node_num=0){
cnt=2;
if(node_num==0) memset(head,0,sizeof(head));
else fill(head,head+node_num+5,0);
}
void add_edge(int u,int v){
G[cnt].to=v;
G[cnt].Next=head[u];
head[u]=cnt++;
}
};
struct AC_automaton{
static const int base=26;
int T[maxn][base],fail[maxn],ed[maxn],pre[maxn],in[maxn],id[maxn],deep[maxn];
queue<int> Q;
int cnt,index;
void insert(char *s){
int p=0;
for(RG i=0;s[i];++i){
if(s[i]=='P'){++ed[p];id[++index]=p;}
else if(s[i]=='B') p=pre[p];
else{
int x=s[i]-'a';
if(!T[p][x]){T[p][x]=++cnt;deep[cnt]=deep[p]+1;}
pre[cnt]=p;p=T[p][x];
}
}
}
void build(Graph &G){
for(RG x=0;x<base;++x)
if(T[0][x]) Q.push(T[0][x]);
while(!Q.empty()){
int p=Q.front();Q.pop();
for(RG x=0;x<base;++x){
int v=T[p][x];
if(v){fail[v]=T[fail[p]][x];Q.push(v);}
else T[p][x]=T[fail[p]][x];
}
}
for(RG u=1;u<=cnt;++u){
// 建立fail树,这里结点编号从1开始,所以trie树上的点编号要加1
G.add_edge(u+1,fail[u]+1);
G.add_edge(fail[u]+1,u+1);
}
}
};
Graph G;
AC_automaton AC;
BIT Tree;
char s[maxn];
int DFN[maxn],Ord[maxn],Ans[maxn],Size[maxn];
bool isY[maxn];
vector<pair<int,int> > Ask[maxn];
int M,Index=0;
void DFS(int u,int fa){// DFS fail树求出fail树的DFS序
DFN[u]=++Index;
Ord[Index]=u;Size[u]=1;
for(int i=G.head[u];i;i=G.G[i].Next){
int v=G.G[i].to;
if(v==fa) continue;
DFS(v,u);
Size[u]+=Size[v];
}
return;
}
void DFSB(int u,int fa){// DFS trie树
Tree.Update(DFN[u],1);// 每DFS到一个点,就在树状数组中给该点加1
if(isY[u]){// 如果当前点是某个询问的y点
for(auto it:Ask[u]){//遍历所有以u为y的询问中的x
int x=it.first,num=it.second;
Ans[num]=Tree.Query(DFN[x],DFN[x]+Size[x]-1);
//使用fail树的DFS序建树状数组
//求出fail树中以x为根的子树中的点权和即为答案
}
}
for(RG i=0;i<26;++i){
int v=AC.T[u-1][i]+1;
if(AC.deep[v-1]<=AC.deep[u-1] || v==fa) continue;
DFSB(v,u);
}
Tree.Update(DFN[u],-1);// 退出该点时在树状数组中减1,消除影响
}
int main(){
scanf("%s",s);
AC.insert(s);
AC.build(G);
Tree.set_Len(AC.cnt+1);
Read(M);
for(RG i=1;i<=M;++i){//对于所有相同的y,我们把询问合并到一起
int x,y;
Read(x);Read(y);
x=AC.id[x]+1;
y=AC.id[y]+1;
Ask[y].push_back(make_pair(x,i));
isY[y]=true;
}
DFS(1,0);// 求出fail树的DFS序
DFSB(1,0);// DFS trie树
for(RG i=1;i<=M;++i)
printf("%d
",Ans[i]);
return 0;
}