带修莫队
往常的莫队都是用一个 (l,r) 来标识当前的状态,然后每次将这个 ([l,r]) 的区间不断扩展、缩小
有了修改,那么可以再增减一维,变成 ([l,r,time]),(time) 可以理解为代表了某个时间的数组的状态,比如做一次修改以后,就让时间加一,因为数组的状态改变了
转移的时候,区间的转移和一般莫队相类似,时间上的转移,就是如果这个时间的修改的点在当前的目标区间(就是当前处理的询问的区间)中,那么就删去数组中的原数,加入修改后的新数,变成新一个状态
然后数组也要改(因为 (time) 变了,当前的数组肯定对应当前的 (time)),为了下面的操作,新数也要和原数交换(比如这一次是“顺向”的进行这个修改,下一次就应该是时间往后退,倒着进行这个修改)
排序方式与块大小、复杂度
类比普通莫队,带修莫队的排序方式应该为以左端点所在的块为第一关键字,右端点所在的块为第二关键字,时间为第三关键字
关于块大小:这似乎才是莫队难的地方,先设块大小为 (d),则一共 (frac{n}{d}) 块,并假设修改查询操作都和数列长度相同,且每次转移 (O(1)),那么分别分析 (l,r,time) 三个指针,注意下面说的一些情况都是 XX 的块 没变,就是他在块内移动,而不是它本身没有移动
- (l) 指针,在块内移动时(这种情况就是因为几个询问左端点相同了,那么按照右端点或时间排序造成的),每次 (O(d)),一个 (n) 次询问,那么就是 (O(nd))
移动到下一个块,每次复杂度 (O(d)),一共 (O(frac{n}{d})) 次,则总复杂度 (O(n)) - (r) 指针,当 (l,r) 都在同一个块,和之前相同,也是 (n) 次每次 (O(d)),一共 (O(nd))
当 (l) 的块没变,(r) 的块改变,移动到下一个块,单次 (O(d)),一共 (O((frac{n}{d})^2)) 次(每次 (l) 的块移动一下,(r) 的块就要回到最左从新移动,所以是块数的平方),总复杂度 (O(frac{n^2}{d}))
当 (l,r) 的块都改变,就是上面说的那种 (r) 的块移回最左边重新移动的情况,一共块数次,每次 (O(n)),那么总复杂度 (O(frac{n^2}{d})) - (time) 指针,当 (l,r) 的块都不变,(time) 因为在这一段使得 (l,r) 的块不变的询问区间内是排好序的,所以对于这一段询问区间是 (O(n)),那么一共有块数的平方个这样的区间,也就是 (O(frac{n^2}{d^2})),那么总复杂度 (O(frac{n^3}{d^2}))
(l) 的块不变,(r) 的块改变,以及 (l,r) 的块都改变,这两种情况都是单次 (O(n)),一共 (O((frac{n}{d})^2)) 和 (O(frac{n}{d})) 次,复杂度则分别为 (O(frac{n^3}{d^2})) 和 (O(frac{n^2}{d}))
综合上面的分析,那么整个算法的复杂度,就是 (O(max(nd,frac{n^2}{d},frac{n^3}{d^2}))),由 (dle n) 推出 (frac{n^2}{d}le frac{n^3}{d^2}),则总复杂度化简为 (O(max(nd,frac{n^3}{d^2})),分类讨论一下这两个谁大,就能很简单的得出 (d=n^{frac{2}{3}}) 时最优,是 (O(n^{frac{5}{3}}))
板子,P1903 [国家集训队]数颜色 / 维护队列:https://www.luogu.com.cn/problem/P1903
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
register int x=0;register int y=1;
register char c=std::getchar();
while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
return y?x:-x;
}
#define N 133340
struct data{
int l,r,id,time;
}q[N];
int qtot;
struct CHANGE{
int pos,x;
}change[N];
int n,m,B;
int block[N],a[N];
int cnt[1000005],ans[N];
inline int cmp(data a,data b){
if(block[a.l]==block[b.l]) return block[a.r]==block[b.r]?a.time<b.time:block[a.r]<block[b.r];
else return block[a.l]<block[b.l];
}
int num;
inline void add(int x){if(++cnt[x]==1) num++;}
inline void del(int x){if(!--cnt[x]) num--;}
inline void work(int time,int i){
if(change[time].pos>=q[i].l&&change[time].pos<=q[i].r){
add(change[time].x);del(a[change[time].pos]);
}
std::swap(change[time].x,a[change[time].pos]);
//这一次被改掉的颜色,就是下一次调用这个函数(参数相同)需要修改成的颜色
}
int main(){
n=read();m=read();B=std::pow(n,2.0/3);
for(reg int i=1;i<=n;i++) a[i]=read(),block[i]=block[i-1]+(!((i-1)%B));
reg int timenow=1;reg char op;
for(reg int i=1;i<=m;i++){
op=getchar();
while(op!='Q'&&op!='R') op=getchar();
if(op=='Q') q[++qtot].l=read(),q[qtot].r=read(),q[qtot].id=qtot,q[qtot].time=timenow;
else{
change[++timenow].pos=read();change[timenow].x=read();
}
}
std::sort(q+1,q+1+qtot,cmp);
reg int l=1,r=0,time=1;
for(reg int nexl,nexr,nexT,i=1;i<=qtot;i++){
nexl=q[i].l;nexr=q[i].r;nexT=q[i].time;
while(r<nexr) add(a[++r]);
while(l>nexl) add(a[--l]);
while(r>nexr) del(a[r--]);
while(l<nexl) del(a[l++]);
while(time<nexT) work(++time,i);//先处理完区间,再处理时间
while(time>nexT) work(time--,i);
ans[q[i].id]=num;
}
for(reg int i=1;i<=qtot;i++) printf("%d
",ans[i]);
}
树上莫队
就是每次询问是一个路径上的某某信息
一般的莫队是按照询问区间的左右端点相关信息来排序,那么树上的莫队,就尝试把树构造成一个序列,把路径变成一个区间
那么可以用到 dfs 序,不过如果直接把 dfs 序写出来,然后对于一个路径找他们端点之间的区间,发现除了包含路径上的点,还包含了一些子树
如何消去这些子树产生的影响?一般的 dfs 序都是一个点进入 dfs 栈的时候,就把他写进 dfs 序中,现在,在一个点出栈的时候,把他往 dfs 序里再写一遍
然后处理这个 dfs 序序列的时候,以往是每往区间加入一个数字,就加上他的贡献,从区间剔除一个数字,就减他的贡献。而现在,同等的对待往区间加入或从区间删除,如果上一次是加贡献,这次就减贡献,反之亦然(具体实现用一个 vis
记录,每次异或)
为什么要这样?如果第二次遇见这个数,遇见的是序列上同一个数(dfs 序序列上每个数都有两个),那么这次减贡献是显然的,如果不是同一个,说明这次遇见的这个数,是标志着这个数出 dfs 栈,那么应该减去贡献来避免刚才说的那些子树对路径上答案的影响
这样,对于那些被包含进去的本不应该存在的子树,消除了他们的贡献
其他的和序列上的莫队一样了
树上带修莫队
就是把在特殊处理的 dfs 序上,按照上面的方式跑莫队改成了跑带修莫队呗
例题:P4074 [WC2013]糖果公园
https://www.luogu.com.cn/problem/P4074
https://darkbzoj.tk/problem/3052
这题如何加上、减去一个数的贡献都是很显然的,就几乎是个树上带修莫队的板子了
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
register int x=0;register int y=1;
register char c=std::getchar();
while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
return y?x:-x;
}
#define N 200005
#define M 200005
struct graph{
int fir[N],nex[M],to[M],tot;
inline void add(int u,int v){
to[++tot]=v;
nex[tot]=fir[u];fir[u]=tot;
}
}G;
int n,m,B;
struct data{
int l,r,time,id;
}q[N];
int qtot;
struct Change{
int pos,val;
}change[N];
int id[N],dfnin[N],dfnout[N],dfscnt;
int deep[N],fa[20][N];
int block[N],col[N],W[N],val[N],vis[N],num[N];
long long Ans[N];
void dfs(int u){
id[++dfscnt]=u;dfnin[u]=dfscnt;
deep[u]=deep[fa[0][u]]+1;
for(reg int i=G.fir[u],v;i;i=G.nex[i]){
v=G.to[i];
if(v==fa[0][u]) continue;
fa[0][v]=u;dfs(v);
}
id[++dfscnt]=u;dfnout[u]=dfscnt;
}
inline int lca(reg int u,reg int v){
if(deep[u]<deep[v]) u^=v,v^=u,u^=v;
for(reg int i=18;~i;i--)if(deep[fa[i][u]]>=deep[v]) u=fa[i][u];
if(u==v) return u;
for(reg int i=18;~i;i--)if(fa[i][u]^fa[i][v]) u=fa[i][u],v=fa[i][v];
return fa[0][u];
}
inline int cmp(data a,data b){
if(block[a.l]==block[b.l]) return block[a.r]==block[b.r]?a.time<b.time:block[a.r]<block[b.r];
return block[a.l]<block[b.l];
}
inline void init_q(int Q){
int timenow=1;
for(reg int i=1,x,y,op;i<=Q;i++){
op=read();x=read();y=read();
if(op){
if(dfnin[x]>dfnin[y]) x^=y,y^=x,x^=y;
q[++qtot]=(data){lca(x,y)==x?dfnin[x]:dfnout[x],dfnin[y],timenow,qtot};
}
else{
change[++timenow]=(Change){x,y};
}
}
std::sort(q+1,q+1+qtot,cmp);
}
LL ans;
inline void add(int x){
if(vis[x]) ans-=(long long)val[col[x]]*W[num[col[x]]--];
else ans+=(long long)val[col[x]]*W[++num[col[x]]];
vis[x]^=1;
}
inline void chtime(Change &x){//传引用!!!
int lastcol=col[x.pos];
if(vis[x.pos]){//计算过这个点,需要先把他的贡献减去,修改完再加回来
add(x.pos);
col[x.pos]=x.val;
add(x.pos);
}
else col[x.pos]=x.val;
x.val=lastcol;
}
int main(){
n=read();m=read();int Q=read();
B=std::pow(2*n,2.0/3);
for(reg int i=1;i<=2*n;i++) block[i]=block[i-1]+(!((i-1)%B));
for(reg int i=1;i<=m;i++) val[i]=read();
for(reg int i=1;i<=n;i++) W[i]=read();
for(reg int i=1,u,v;i<n;i++){
u=read();v=read();
G.add(u,v);G.add(v,u);
}
for(reg int i=1;i<=n;i++) col[i]=read();
dfs(1);
for(reg int i=1;i<=18;i++)
for(reg int j=1;j<=n;j++) fa[i][j]=fa[i-1][fa[i-1][j]];
init_q(Q);
reg int l=1,r=0,time=1;
for(reg int i=1,nexl,nexr,nexT;i<=qtot;i++){
// printf("now : %d %d %d
",l,r,time);
nexl=q[i].l;nexr=q[i].r;nexT=q[i].time;
while(r<nexr) add(id[++r]);
while(l>nexl) add(id[--l]);
while(r>nexr) add(id[r--]);
while(l<nexl) add(id[l++]);
while(time<nexT) chtime(change[++time]);
while(time>nexT) chtime(change[time--]);
int Lca=lca(id[l],id[r]);
if((Lca^id[l])&&(Lca^id[r])){//lca 需要另外计算
add(Lca);
Ans[q[i].id]=ans;
add(Lca);
}
else Ans[q[i].id]=ans;
// printf("id : %d ans : %lld
",q[i].id,ans);
}
for(reg int i=1;i<=qtot;i++) printf("%lld
",Ans[i]);
return 0;
}
在线莫队
在线莫队的操作是从这里看的,orz 诗乃:https://www.luogu.com.cn/blog/asadashino/moqueue
一般的莫队是把询问排序的,打乱无法保证在原顺序中,回答下一个询问前一定知道上一个询问的答案,所以强制在线的情况下想排序询问肯定不可能,但是莫队那种转移区间的方式还是可以用的
方法就是选取一些“特征点”,然后预处理任意两个“特征点”之间的答案和转移这些区间要用到的信息,称之为特征区间,设最小特征区间的长度为 (d),那么预处理的复杂度就是 (O(ncdot frac{n}{d})=O(frac{n^2}{d}))(就是固定住一个点,另一个点不断向外扩展到下一个特征点,然后扩展到最右就移动固定的点,固定的点移动 (frac{n}{d}) 次),然后询问就通过移动区间到任意一个特征区间,单次复杂度 (O(d)),显然 (d=sqrt n) 时最优,那么总复杂度依然 (O(nsqrt n))
所以其实还是比较简单的
但是之前说过要维护特征区间的转移信息,肯定不能每个区间都单独维护,如果这个信息满足可减性,那么直接用前缀和的思想减一下就行,否则可能得用可持久化等东西?反正我不会就是了
在线带修莫队
去看诗乃的博客吧,看了看好复杂的样子