1 条题解
-
0
自动搬运
来自洛谷,原作者为

Alex_Wei
**搬运于
2025-08-24 22:12:24,当前版本为作者最后更新于2021-02-15 10:13:16,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
题意简述:给定长度为 的文本串 和有 个单词的字典 。 次操作,每次求出字典内所有单词在 的出现次数,或将 替换为 不断重复的结果。
,,,。
假设没有修改操作,且 ,即字典中只有一个单词,记该单词长度为 。 一个自然的想法是可以求出对于每个位置 , 是否匹配 ,记为 ,暴力匹配即可。
查询时,因为 长度不够,显然无法匹配 ,又因为 匹配时不受小于 的位置的影响,即不会因为 消失而导致 的匹配情况改变(由于 ,所以 )。因此直接查询 即可。直接前缀和维护可以做到 ,使用 KMP 可以优化到 。
Subtask #3:如果有修改操作呢?事情就变得无比麻烦了。昨天尝试写了一下,甚至比正解还难写(doge)。
首先得处理这个循环的 。
当然,我不知道怎么处理,所以看了眼 s_r_f 的题解。注意到题目中说 “与文件相比,单词的长度是非常小的()”,那么就好好利用一下这个性质。首先, 肯定是没有什么好方法,只能暴力硬做,那么,这一部分的想法是:从 开始跑 KMP,匹配到 并暴力更新 即可。为什么要从 而不是 开始跑: 是与 有关的,所以从 开始跑会导致 全为 ,这显然是错误的。
同样的,对于 ,从 开始跑 KMP,一直匹配到 并暴力更新 即可。
接下来处理中间那一大坨循环的 ,记其长度为 。有一个并不显然但很好理解,同时也是最关键的性质:修改过后,,也就是这部分 会产生长度为 的循环节。根据题目所给条件,有 ( 的范围同上),所以显然。因此,求出循环节并用线段树维护即可。
当然,说起来简单,还有很多需要注意的地方:
-
Q1:如何维护循环节?
A1:一个循环节信息主要就是循环节的开头位置,长度和循环节内每个位置的值。 因此,需要维护每一个循环节 (表示这是第 次修改形成的循环节)的开头位置 ,长度 ,以及每一个位置 上的值 。循环节内位置从 开始标号。
在线段树区间 的懒标记内维护两个信息: 和 。 表示该标记是第 次修改形成的循环节(不是第 个循环节), 表示该区间的起始位置 在该循环节中的位置。
在 pushdown 的时候,假设我们传入了三个参数 表示从区间 下传,该区间在线段树内编号为 。先求出左区间和右区间的分界点 ,再求出右区间 的起始位置 在循环节的位置,记为 ,则不难求出 ,然后将 与 的懒标记分别赋给 与 ,并更新其维护的区间 之和:
void push(int l,int r,int x){ // pushdown if(laz[x].id){ int m=l+r>>1; int id=laz[x].id,hd=laz[x].hd; int mid=(hd+(m-l+1))%len[id]; mark(l,m,x<<1,id,hd); mark(m+1,r,x<<1|1,id,mid); val[x]=val[x<<1]+val[x<<1|1]; laz[x].id=0; } }其中
mark(l,r,x,id,hd)表示给在线段树内编号为 的区间 打上 的懒标记。此时我们求出该区间末位置在循环节 中的位置 ,并求出 间共有多少个循环节 ,然后更新即可。void mark(int l,int r,int x,int id,int hd){ // mark lazytag & update laz[x]={id,hd}; int ori=l-hd,tl=(r-ori)%len[id],rid=(r-ori)/len[id]; if(rid==0)val[x]=pre[id][tl]-(hd?pre[id][hd-1]:0); else val[x]=pre[id][tl]+(rid*sum[id]-(hd?pre[id][hd-1]:0)); }一些代码说明: 表示 所在的循环节的开头,并假设其为第 个循环节。 表示 在第 个循环节内。需要注意的是,这里的 是 的前缀和。 表示循环节 所有位置上的和,即 。
-
Q2:暴力匹配时怎么求出 当前的内容?
首先,需要求出的 是一段区间,设其为 。那么在线段树上按序遍历每一个区间 。具体来说,就算当前区间 也不返回,直到访问到叶子结点 。可以证明这样访问的时间复杂度为 。又因为需要求出的长度不超过 ,因此总时间复杂度为 。
若该区间有标记 ,那么该位置上的字符应为 ,即第 次修改时给出的字符串 的第 个位置上的字符。否则该位置上的字符应为 。
void forms(int l,int r,int ql,int qr,int x){ // form current string [ql,qr] if(ql>qr)return; if(l==r){ if(laz[x].id)tmp+=qt[laz[x].id][laz[x].hd]; else tmp+=ct[l-1]; return; } int m=l+r>>1; push(l,r,x); if(ql<=m)forms(l,m,ql,qr,x<<1); if(m<qr)forms(m+1,r,ql,qr,x<<1|1); } -
Q3:暴力匹配求出 后怎么在线段树上修改?
A3:同样,需要更新的 也是一段区间 。如法炮制,按序遍历每一个区间 ,并修改 的值即可。
由于求出 的内容时需要每个节点的标记,而暴力修改 的位置有的需要打标记,如 ,有的不需要,如 ,所以需要分情况讨论。
void modifyc(int l,int r,int ql,int qr,int x,bool tg){ // brute force : change if(ql>qr)return; if(l==r){ val[x]=f[l]; if(tg)laz[x]={id,(l-lpos)%len[id]}; return; } int m=l+r>>1; push(l,r,x); if(ql<=m)modifyc(l,m,ql,qr,x<<1,tg); if(m<qr)modifyc(m+1,r,ql,qr,x<<1|1,tg); val[x]=val[x<<1]+val[x<<1|1]; }
还有一些注意点(踩过的坑):
- 正如 s_r_f 所说,为了使代码更简洁,我们可以将左边暴力匹配的区间 的右端点向右稍微移动一点,使得新的右端点 刚好是一段循环节的结尾。同时,为了求出循环节,还要再向右匹配 个位置。
- 如果区间的长度太小,可以直接暴力匹配。
- 需要先求出 再更新,因为更新时需要用到 。
这样,时间复杂度为 。
看到这里,你可能以为我已经讲完了。实际上并没有,这只是 的部分分。不过别担心,只要你会 AC 自动机,那么 为多少都不是问题。
注意到字典是固定的,所以我们对其建立 AC 自动机。那么只需要将 的定义改为:将 放在 AC 自动机上跑到的位置 在 fail 树上与根节点之间的路径所包含的终止节点个数 。即 $val_p=\sum_{i=1}^m [endpos_i\in \mathrm{path}(p,root)]$。一个套路的方法是将所有终止节点在 fail 树上的子树的 值 ,这样可以 求 。如果不理解上述方法,P5357,请。
注意到 定义中的 可以改成 ,因为任何一个单词 与 在位置 的匹配情况不会受到 的影响(最长的单词与 在位置 的匹配的第一个位置为 )。
剩下来就和 几乎一模一样,只不过在暴力匹配时的方式从跑 KMP 变成了跑 AC 自动机。总时间复杂度 ,其中 。
/* Powered by C++11. Author : Alex_Wei. */ #include <bits/stdc++.h> using namespace std; #define ll long long const int N=1e6+5; const int L=50+5; const int Q=1e5+5; const int S=2e5+5; // basic variables int lc,nw,q; ll mp[1<<7],f[N]; string ct; struct ACAM{ int cnt,son[S][62],fa[S],val[S]; void ins(string s){ int p=0; for(char it:s){ if(!son[p][mp[it]])son[p][mp[it]]=++cnt; p=son[p][mp[it]]; } val[p]++; } void build(){ queue <int> q; for(int i=0;i<62;i++)if(son[0][i])q.push(son[0][i]); while(!q.empty()){ int t=q.front(); q.pop(); for(int i=0;i<62;i++) if(son[t][i])fa[son[t][i]]=son[fa[t]][i],q.push(son[t][i]); else son[t][i]=son[fa[t]][i]; val[t]+=val[fa[t]]; } } void run(){ // get f int p=0; for(int i=1;i<=lc;i++)f[i]=val[p=son[p][mp[ct[i-1]]]]; } }ac; // query variables ll id,len[Q],sum[Q]; vector <ll> pre[Q]; string qt[Q]; // lazytag & Segment Tree string tmp; struct lazy{ int id,hd; }; struct SegTree{ ll val[N<<2],lpos; lazy laz[N<<2]; void build(int l,int r,int x){ if(l==r){ val[x]=f[l]; return; } int m=l+r>>1; build(l,m,x<<1),build(m+1,r,x<<1|1); val[x]=val[x<<1]+val[x<<1|1]; } void mark(int l,int r,int x,int id,int hd){ // mark lazytag & update laz[x]={id,hd}; int ori=l-hd,tl=(r-ori)%len[id],rid=(r-ori)/len[id]; if(rid==0)val[x]=pre[id][tl]-(hd?pre[id][hd-1]:0); else val[x]=pre[id][tl]+(rid*sum[id]-(hd?pre[id][hd-1]:0)); } void push(int l,int r,int x){ // pushdown if(laz[x].id){ int m=l+r>>1; int id=laz[x].id,hd=laz[x].hd; int mid=(hd+(m-l+1))%len[id]; mark(l,m,x<<1,id,hd); mark(m+1,r,x<<1|1,id,mid); laz[x].id=0; } } void modifyt(int l,int r,int ql,int qr,int x){ // tag if(ql<=l&&r<=qr){ mark(l,r,x,id,(l-lpos)%len[id]); return; } int m=l+r>>1; push(l,r,x); if(ql<=m)modifyt(l,m,ql,qr,x<<1); if(m<qr)modifyt(m+1,r,ql,qr,x<<1|1); val[x]=val[x<<1]+val[x<<1|1]; } void modifyc(int l,int r,int ql,int qr,int x,bool tg){ // brute force : change if(ql>qr)return; if(l==r){ val[x]=f[l]; if(tg)laz[x]={id,(l-lpos)%len[id]}; return; } int m=l+r>>1; push(l,r,x); if(ql<=m)modifyc(l,m,ql,qr,x<<1,tg); if(m<qr)modifyc(m+1,r,ql,qr,x<<1|1,tg); val[x]=val[x<<1]+val[x<<1|1]; } ll query(int l,int r,int ql,int qr,int x){ if(ql>qr)return 0; if(ql<=l&&r<=qr)return val[x]; ll m=l+r>>1,ans=0; push(l,r,x); if(ql<=m)ans+=query(l,m,ql,qr,x<<1); if(m<qr)ans+=query(m+1,r,ql,qr,x<<1|1); return ans; } void forms(int l,int r,int ql,int qr,int x){ // form current string [ql,qr] if(ql>qr)return; if(l==r){ if(laz[x].id)tmp+=qt[laz[x].id][laz[x].hd]; else tmp+=ct[l-1]; return; } int m=l+r>>1; push(l,r,x); if(ql<=m)forms(l,m,ql,qr,x<<1); if(m<qr)forms(m+1,r,ql,qr,x<<1|1); } }st; int main(){ // freopen("P5599_5.in","r",stdin); // freopen("P5599_5.out","w",stdout); // mp for(int i='A';i<='Z';i++)mp[i]=i-'A'; for(int i='a';i<='z';i++)mp[i]=26+(i-'a'); for(int i='0';i<='9';i++)mp[i]=52+(i-'0'); // read & init cin>>lc>>nw>>q>>ct; for(int i=1;i<=nw;i++){ string wrd; cin>>wrd,ac.ins(wrd); } ac.build(),ac.run(),st.build(1,lc,1); // solve for(int i=1;i<=q;i++){ int op,l,r,ls; scanf("%d%d%d",&op,&l,&r),ls=r-l+1,st.lpos=l; if(op==1){ ll rpos=min(r,l+L),ans=0,p=0; tmp="",st.forms(1,lc,l,rpos,1); for(int i=l;i<=rpos;i++)ans+=ac.val[p=ac.son[p][mp[tmp[i-l]]]]; printf("%lld\n",ans+st.query(1,lc,rpos+1,r,1)); } else{ string t; cin>>t,len[++id]=t.size(),pre[id].resize(len[id]); int lpos=max(1,l-L+1),rpos=min(lc,r+L-1),p=0; if(ls<=L*2+len[id]*2){ tmp="",st.forms(1,lc,lpos,rpos,1); // get current string for(int i=lpos;i<=rpos;i++){ char it=(i<l||i>r?tmp[i-lpos]:t[(i-l)%len[id]]); p=ac.son[p][mp[it]]; if(i>=l)f[i]=ac.val[p]; } st.modifyc(1,lc,l,r,1,1),st.modifyc(1,lc,r+1,rpos,1,0); } else{ // front section : [l-L+1,l+L-1] int led=l+L-1,rbg=r-L+1; while((led-l)%len[id])led++; tmp="",st.forms(1,lc,lpos,l-1,1); for(int i=lpos;i<led+len[id];i++){ char it=(i<l?tmp[i-lpos]:t[(i-l)%len[id]]); p=ac.son[p][mp[it]]; if(i>=l){ if(i<led)f[i]=ac.val[p]; else pre[id][i-led]=(i>led?pre[id][i-led-1]:0)+ac.val[p]; } } sum[id]=pre[id][len[id]-1]; st.modifyc(1,lc,l,led-1,1,1),st.modifyt(1,lc,led,r,1); // back section tmp="",st.forms(1,lc,r+1,rpos,1),p=0; for(int i=rbg;i<=rpos;i++){ char it=(i>r?tmp[i-r-1]:t[(i-l)%len[id]]); p=ac.son[p][mp[it]]; if(i>r)f[i]=ac.val[p]; } st.modifyc(1,lc,r+1,rpos,1,0); } qt[id]=t; } } return 0; } 如果您看懂了这篇题解就点个赞吧 qwq。 -
- 1
信息
- ID
- 4605
- 时间
- 1000~2000ms
- 内存
- 512MiB
- 难度
- 7
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者