先想想暴力怎么搞
搞一个AC自动机
对每个询问 x,y
把 y 暴力向下匹配
每个点都暴力跳fail
看看x出现了几次
稍微优化一波
因为有多组询问
考虑离线
可以把同一组的 y 一起来计算
还是把 y 暴力匹配
看看所有的 x 出现了几次
再来一波优化
考虑什么时候 x 的出现次数会增加
显然是在 y 的某个节点的 fail 路径上
因为每个点只有一个 fail
所以
所有的 fail 构成了一颗树
如果把 fail 看成无向边,根节点为自动机的根节点
那就相当于问 在fail树的x的结束节点的子树中,有几个节点属于y
那么询问 x,y 就只要在AC自动机上跑到y
路过的每一个节点就把 记录值+1
然后询问 在fail树中 x 的结束节点的子树 记录值之和为多少
对于这种对子树的询问
用什么方法最好呢?
树链剖分!
为什么这么麻烦
虽然不可能用树剖
但是可以用树剖的思想
给每个节点一个dfs序
在AC自动机上跑的时候
每经过一个节点就把该节点的dfs序的值+1
退出该节点时就-1
然后询问就像树剖的子树询问一样了
因为每次是单点修改,区间求和
所以用树状数组维护一波就好了
总结一下
把每个操作离线
把y相同的询问放在一起
dfs一波,每次找到结束标记
就把相关的询问处理
具体的实现在代码里
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #include<queue> using namespace std; inline int read() { int x=0; char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x; } const int N=1e5+7; char s[N]; int fa[N]; int n,m; int c[N][27],pd[N],fail[N],cnt,las[N];//las[i]表示第i个串的结束节点的位置 inline void build()//处理操作并构造出AC自动机 { scanf("%s",s); int u=0,len=strlen(s); for(int i=0;i<len;i++) { if(s[i]=='B') u=fa[u];//回到上一个位置相当于删除最后一个单词 if(s[i]=='P') pd[u]=++n,las[n]=u;//记录一波 if(s[i]!='B'&&s[i]!='P') { int v=s[i]-'a'+1; if(!c[u][v]) c[u][v]=++cnt; fa[c[u][v]]=u; u=c[u][v];//记录上一个位置并向下走 } } } //以下为预处理fail并构造出fail树 queue <int> q; int fir[N],from[N],to[N],cnt2;//存fail树 inline void Add(int a,int b) { from[++cnt2]=fir[a]; fir[a]=cnt2; to[cnt2]=b; }//向fail树中加边 int C[N][27];//存原AC自动机的结构,因为处理fail时会把AC自动机的结构改变 //等等还要用原来的结构来dfs处理询问 void pre() { for(int i=0;i<=cnt;i++) for(int j=1;j<=26;j++) C[i][j]=c[i][j];//拷贝一波 for(int i=1;i<=26;i++) if(c[0][i]) q.push(c[0][i]),Add(0,c[0][i]);//从根到这些节点也有fail边 //加边时加单向边就好了,没影响 while(!q.empty()) { int u=q.front(); q.pop(); for(int i=1;i<=26;i++) { int v=c[u][i]; if(!v) c[u][i]=c[fail[u]][i]; else fail[v]=c[fail[u]][i],Add(fail[v],v),q.push(v);//预处理fail并构造出fail树,重要操作 } } } //以上为预处理fail并构造出fail树 //第一波dfs确定dfs序 int dfn[N],sz[N],cnt3;//dfn是dfs序,sz是子树大小 void dfs1(int x) { dfn[x]=++cnt3; sz[x]=1; for(int i=fir[x];i;i=from[i]) dfs1(to[i]),sz[x]+=sz[to[i]]; } //以下为树状数组 int t[N]; inline void add(int x,int v){ while(x<=cnt3) t[x]+=v,x+=x&-x; } inline int query(int x) { int res=0; while(x) res+=t[x],x-=x&-x; return res; } //以上为树状数组 //以下存询问 struct data { int x,y,id,ans; }d[N]; inline bool cmp(const data &a,const data &b){ return a.y<b.y; } int l[N],r[N];//l[i]表示排序后y值为i的区间的左端点,r为右端点 //以上存询问 void dfs2(int x) { add(dfn[x],1); if(pd[x])//如果找到结束标记 for(int i=l[pd[x]];i<=r[pd[x]];i++)//把所有相关的询问都处理掉,显然此时只有属于y的节点有1的值 d[i].ans=query( dfn[ las[d[i].x] ]+sz[ las[d[i].x] ]-1 )-query( dfn[ las[d[i].x] ]-1 );//像树剖一样询问,注意减1 for(int i=1;i<=26;i++) { int v=C[x][i];//在原来的自动机上跑 if(!v) continue;//可能后面没有节点了,不能走 dfs2(v); } add(dfn[x],-1);//退出时值要改回来 } int Ans[N]; int main() { build(); pre(); //读入询问并处理l,r cin>>m; for(int i=1;i<=m;i++) d[i].x=read(),d[i].y=read(),d[i].id=i; sort(d+1,d+m+1,cmp); l[d[1].y]=1; for(int i=2;i<=m;i++) if(d[i].y!=d[i-1].y) { r[d[i-1].y]=i-1; l[d[i].y]=i; } r[d[m].y]=m; dfs1(0);//确定dfs序 dfs2(0);//dfs处理询问 for(int i=1;i<=m;i++) Ans[d[i].id]=d[i].ans;//按原来的顺序把答案放到答案数组里 for(int i=1;i<=m;i++) printf("%d ",Ans[i]); return 0; }