题目传送门

题意简述:给定长度为 \(n\) 的文本串 \(a\) 和有 \(m\) 个单词的字典 \(s_i\)。\(q\) 次操作,每次求出字典内所有单词在 \(a[l,r]\) 的出现次数,或将 \(a[l,r]\) 替换为 \(t\) 不断重复的结果。

\(n,\sum |t|\leq10^6\),\(m,q\leq 10^5\),\(|s|\leq 50\),\(\sum |s_i|\leq 2\times 10^5\)。


假设没有修改操作,且 \(m=1\),即字典中只有一个单词,记该单词长度为 \(L\)。

一个自然的想法是可以求出对于每个位置 \(i\),\(a[i-L+1,i]\) 是否匹配 \(s_1\),记为 \(f_i\ (f_1,f_2,\cdots,f_{L-1}=0)\),暴力匹配即可。

查询时,因为 \([l,l],[l,l+1],\cdots,[l,l+L-2]\) 长度不够,显然无法匹配 \(s_1\),又因为 \([x-L+1,x]\ (x\geq l+L-1)\) 匹配时不受小于 \(l\) 的位置的影响,即不会因为 \(a[1,l-1]\) 消失而导致 \([x-L+1,x]\) 的匹配情况改变(由于 \(x\geq l+L-1\),所以 \(x-L+1\geq l\))。因此直接查询 \(\sum_{j=x}^rf_j\) 即可。直接前缀和维护可以做到 \(\mathcal{O}(q+nL)\),使用 KMP 可以优化到 \(\mathcal{O}(q+n)\)。


Subtask #3:如果有修改操作呢?事情就变得无比麻烦了。昨天尝试写了一下,甚至比正解还难写(doge)。

首先得处理这个循环的 \(t\)。当然,我不知道怎么处理,所以看了眼 s_r_f 的题解。 注意到题目中说 “与文件相比,单词的长度是非常小的(\(L\leq 50\))”,那么就好好利用一下这个性质。

首先,\(f_{[l,l+L-2]}\) 肯定是没有什么好方法,只能暴力硬做,那么,这一部分的想法是:从 \(l-L+1\) 开始跑 KMP,匹配到 \(l+L-2\) 并暴力更新 \(f_{[l,l+L-2]}\) 即可。为什么要从 \(l-L+1\) 而不是 \(l\) 开始跑:\(f_l\) 是与 \(a_{l-L+1}\) 有关的,所以从 \(l\) 开始跑会导致 \(f_{[l,l+L-2]}\) 全为 \(0\),这显然是错误的。

同样的,对于 \(f_{[r+1,r+L-1]}\),从 \(r-L+2\) 开始跑 KMP,一直匹配到 \(r+L-1\) 并暴力更新 \(f_{[r+1,r+L-1]}\) 即可。

接下来处理中间那一大坨循环的 \(t\),记其长度为 \(T\)。有一个并不显然但很好理解,同时也是最关键的性质:修改过后,\(f_i=f_{i+T}\ (l+L-1\leq i\leq r-T+1)\),也就是这部分 \(f\) 会产生长度为 \(T\) 的循环节。根据题目所给条件,有 \(a_{[i,i-L+1]}=a_{[i+T,(i+T)-L+1]}\)(\(i\) 的范围同上),所以显然。因此,求出循环节并用线段树维护即可。

当然,说起来简单,还有很多需要注意的地方:

  • Q1:如何维护循环节?

    A1:一个循环节信息主要就是循环节的开头位置,长度和循环节内每个位置的值。 因此,需要维护每一个循环节 \(id\)(表示这是第 \(id\) 次修改形成的循环节)的开头位置 \(lp_{id}\),长度 \(len_{id}\),以及每一个位置 \(i\) 上的值 \(d_{id,i}\)。循环节内位置从 \(0\) 开始标号

    在线段树区间 \([l,r]\) 的懒标记内维护两个信息:\(id\) 和 \(hd\)。\(id\) 表示该标记是第 \(id\) 次修改形成的循环节(不是第 \(id\) 个循环节),\(hd\) 表示该区间的起始位置 \(l\) 在该循环节中的位置。

    在 pushdown 的时候,假设我们传入了三个参数 \(l,r,x\) 表示从区间 \([l,r]\) 下传,该区间在线段树内编号为 \(x\)。先求出左区间和右区间的分界点 \(m=\lfloor\frac{l+r}{2}\rfloor\),再求出右区间 \([m+1,r]\) 的起始位置 \(m+1\) 在循环节的位置,记为 \(mid\),则不难求出 \(mid=(hd+(m+1)-l)\bmod len_{id}\),然后将 \(id,hd\) 与 \(id,mid\) 的懒标记分别赋给 \([l,m]\) 与 \([m+1,r]\),并更新其维护的区间 \(f\) 之和:

      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) 表示给在线段树内编号为 \(x\) 的区间 \([l,r]\) 打上 \(id,hd\) 的懒标记。此时我们求出该区间末位置在循环节 \(id\) 中的位置 \(tl=(hd+(r-l))\bmod len_{id}\),并求出 \([l,r]\) 间共有多少个循环节 \(id\),然后更新即可。

     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));
    }

    一些代码说明:\(ori\) 表示 \(l\) 所在的循环节的开头,并假设其为第 \(0\) 个循环节。\(rid\) 表示 \(r\) 在第 \(rid\) 个循环节内。需要注意的是,这里的 \(pre_{id,i}\) 是 \(pre_{id}\) 的前缀和。\(sum_{id}\) 表示循环节 \(id\) 所有位置上的和,即 \(sum_{id}=pre_{id,len_{id}-1}\)。

  • Q2:暴力匹配时怎么求出 \(a\) 当前的内容?

    首先,需要求出的 \(a\) 是一段区间,设其为 \([l,r]\)。那么在线段树上按序遍历每一个区间 \([x,x]\ (l\leq x\leq r)\)。具体来说,就算当前区间 \([l',r']\subseteq[l,r]\) 也不返回,直到访问到叶子结点 \([x,x]\)。可以证明这样访问的时间复杂度为 \(\mathcal{O}(\log n+len)\)。又因为需要求出的长度不超过 \(2L\),因此总时间复杂度为 \(\mathcal{O}(q(\log n+L))\)。

    若该区间有标记 \(id,hd\),那么该位置上的字符应为 \(t_{id,hd}\),即第 \(id\) 次修改时给出的字符串 \(t_{id}\) 的第 \(hd\) 个位置上的字符。否则该位置上的字符应为 \(a_x\)。

      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:暴力匹配求出 \(f\) 后怎么在线段树上修改?

    A3:同样,需要更新的 \(f\) 也是一段区间 \([l,r]\)。如法炮制,按序遍历每一个区间 \([x,x]\ (l\leq x\leq r)\),并修改 \(f\) 的值即可。

    由于求出 \(a\) 的内容时需要每个节点的标记,而暴力修改 \(f\) 的位置有的需要打标记,如 \([l,l+L-2]\),有的不需要,如 \([r+1,r+L-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];
    }

还有一些注意点(踩过的坑):

  • 正如 s_r_f 所说,为了使代码更简洁,我们可以将左边暴力匹配的区间 \([l-L+1,l+L-2]\) 的右端点向右稍微移动一点,使得新的右端点 \(led\) 刚好是一段循环节的结尾。同时,为了求出循环节,还要再向右匹配 \(T\) 个位置。
  • 如果区间的长度太小,可以直接暴力匹配。
  • 需要先求出 \(sum_{id}\) 再更新,因为更新时需要用到 \(sum_{id}\)。

这样,时间复杂度为 \(\mathcal(q(\log n+L)+\sum T)\)。


看到这里,你可能以为我已经讲完了。实际上并没有,这只是 \(m=1\) 的部分分。不过别担心,只要你会 AC 自动机,那么 \(m\) 为多少都不是问题。

注意到字典是固定的,所以我们对其建立 AC 自动机。那么只需要将 \(f_i\) 的定义改为:将 \(a[1,i]\) 放在 AC 自动机上跑到的位置 \(p\) 在 fail 树上与根节点之间的路径所包含的终止节点个数 \(val_p\)。即 \(val_p=\sum_{i=1}^m [endpos_i\in \mathrm{path}(p,root)]\)。一个套路的方法是将所有终止节点在 fail 树上的子树的 \(val\) 值 \(+1\),这样可以 \(\mathcal{O}(1)\) 求 \(f_i\)。如果不理解上述方法,P5357,

注意到 \(f_i\) 定义中的 \(a[1,i]\) 可以改成 \(a[i-L+1,i]\ (L=\max|s_j|)\),因为任何一个单词 \(s_j\) 与 \(a\) 在位置 \(i\) 的匹配情况不会受到 \(a_x\ (x\leq i-L)\) 的影响(最长的单词与 \(a\) 在位置 \(i\) 的匹配的第一个位置为 \(i-L+1\))

剩下来就和 \(m=1\) 几乎一模一样,只不过在暴力匹配时的方式从跑 KMP 变成了跑 AC 自动机。总时间复杂度 \(\mathcal{O}(\sum|s_i|+\sum|t_i|+q(\log n+L))\),其中 \(L=\max |s_i|\)。

/*
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;
}

P5599【XR-4】文本编辑器的更多相关文章

  1. bbs项目引入富文本编辑器和处理xss攻击和文章预览

    一.富文本编辑上传文章和图片 富文本编辑器我们使用kindeditor,我们首先去官网下载,然后解压,放到我们的static的目录中 然后我们在html中这样使用富文本编辑器 <!DOCTYPE ...

  2. [bzoj1269]文本编辑器editor [bzoj1500]维修数列

    1269: [AHOI2006]文本编辑器editor Time Limit: 10 Sec Memory Limit: 162 MB Submit: 2540 Solved: 923 [Submit ...

  3. 富文本编辑器Simditor的简易使用

    最近打算自己做一个博客系统,并不打算使用帝国cms或者wordpress之类的做后台管理!自己处于学习阶段也就想把从前台到后台一起谢了.好了,废话不多说了,先来看看富文本编辑器SimDitor,这里是 ...

  4. 个人网站对xss跨站脚本攻击(重点是富文本编辑器情况)和sql注入攻击的防范

    昨天本博客受到了xss跨站脚本注入攻击,3分钟攻陷--其实攻击者进攻的手法很简单,没啥技术含量.只能感叹自己之前竟然完全没防范. 这是数据库里留下的一些记录.最后那人弄了一个无限循环弹出框的脚本,估计 ...

  5. 关于SMARTFORMS文本编辑器出错

    最近在做ISH的一个打印功能,SMARTFORM的需求本身很简单,但做起来则一波三折. 使用环境是这样的:Windows 7 64bit + SAP GUI 740 Patch 5 + MS Offi ...

  6. 基于trie树的具有联想功能的文本编辑器

    之前的软件设计与开发实践课程中,自己构思的大作业题目.做的具有核心功能,但是还欠缺边边角角的小功能和持久化数据结构,先放出来,有机会一点点改.github:https://github.com/chu ...

  7. UEditor百度富文本编辑器--让编辑器自适应宽度的解决方案

    UEditor百度富文本编辑器的initialFrameWidth属性,默认值是1000. 不能够自适应屏幕宽度.如图1: 刚开始的时候,我是直接设置initialFrameWidth=null的.效 ...

  8. [bzoj1269][AHOI2006文本编辑器editor] (splay模版题 or pb_ds [rope]大法)

    Description 这些日子,可可不和卡卡一起玩了,原来可可正废寝忘食的想做一个简单而高效的文本编辑器.你能帮助他吗?为了明确任务目标,可可对“文本编辑器”做了一个抽象的定义:   文本:由0个或 ...

  9. Bzoj1269 [AHOI2006]文本编辑器editor

    Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 3678  Solved: 1380 Description 这些日子,可可不和卡卡一起玩了,原来可可正 ...

  10. PHP Ueditor 富文本编辑器

    2016年12月11日 08:46:59 星期日 百度的简版富文本编辑器umeditor很久没更新了 全功能版本的配置项跟umeditor还是有区别的, 这里说下ueditor怎么对接到项目中去, 主 ...

随机推荐

  1. 【UE4 C++】 UnrealPak 与 Pak 的制作、挂载、加载

    简介 通过 UnrealPak,可以将资源打包成 Pak 文件 Pak文件是UE4游戏生成的数据包文件. Pak 之前一般先有 Cooked 步骤,将资源烘焙为对应平台支持的资源 一般打包后的项目使用 ...

  2. Wireshark 过滤器的使用

    符号 例子 = = tcp.port = = 80 过滤出来TCP包含80端口的数据包 != ip.src != 127.0.0.1 ip的原地址不是127.0.0.1过滤出来 > lp.len ...

  3. 【二食堂】Beta - 发布声明

    Beta - 发布声明 新功能 在Beta阶段,图谱方面的新功能有:自定义关系的添加与删除.实体查找.实体名称的修改.实体之间关系的修改.新增了项目创建与删除功能,此外还增加了好友系统,可以实现好友的 ...

  4. no_code团队介绍和bingduoduo项目采访

    项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 要求 团队作业-团队介绍和采访 成员简介 name avatar intro PM Dev Test UI/Front-End 伦泽标 ...

  5. 最容易出错的C语言指针

    C语言指针说难不难但是说容易又是最容易出错的地方,因此不管是你要做什么只要用到C指针你就跳不过,今天咱们就以 十九个例子来给大家简单的分析一下指针的应用,最后会有C语言视频资料提供给大家更加深入的参考 ...

  6. TCP/IP参考模型(应用层、传输层、网际层、网络接口层)、五层参考模型(应用层、传输层、网络层、数据链路层、物理层)、OSI与TCP/IP参考模型比较

    文章转自:https://blog.csdn.net/weixin_43914604/article/details/104597450 学习课程:<2019王道考研计算机网络> 学习目的 ...

  7. Spring源码分析-BeanFactoryPostProcessor

    Spring源码分析-BeanFactoryPostProcessor 博主技术有限,本文难免有错误的地方,如果您发现了欢迎评论私信指出,谢谢 BeanFactoryPostProcessor接口是S ...

  8. Spring Cloud Alibaba 使用Nacos作为配置管理中心

    为什么需要配置中心? 动态配置管理是 Nacos 的三大功能之一,通过动态配置服务,我们可以在所有环境中以集中和动态的方式管理所有应用程序或服务的配置信息. 动态配置中心可以实现配置更新时无需重新部署 ...

  9. STL 去重 unique

    一.unique函数 类属性算法unique的作用是从输入序列中"删除"所有相邻的重复元素. 该算法删除相邻的重复元素,然后重新排列输入范围内的元素,并且返回一个迭代器(容器的长度 ...

  10. VNC服务器的搭建(带图形化支持)

    环境:centos7.6最小化安装 图形化支持 如果希望安装简单的图形支持的话,仅包含gnome的最最最最基础的包的话可以使用以下命令 yum groups install "X Windo ...