带修莫队分块
带修莫队分块
\(\text{Update 2021/7/12}\)
- 填坑,找到一道带修莫队的题目。
\(\text{Update 2021/7/10}\)
- 更新了码疯,用
while
语句替代了莫队中的for
语句。 - 修改了错别字和病句。
在这篇博客中,我已经介绍了“静态莫队”算法,它可以离线解决一类静态(不带修改)的区间问题。
经过后人的不断完善,出现了“带修莫队”,让莫队可以支持修改操作。
什么是带修莫队?什么是带修莫队?如果你想了解,什么是带修莫队的话,现在就带你研究。
Part 1 带修莫队原理
引入时间轴
带修莫队和普通莫队的基本原理大同小异,都是排序后优化访问顺序,然后暴力。
因为要支持修改操作,带修莫队除了要知道查询区间的位置之外,还要知道它在什么时候进行查询,以便把序列更新到这次查询时应有的状态。于是在询问的结构体中多开一个变量 \(t\) ,用来记录这次询问之前第一个修改操作的位置。也就是说,这次查询基于第 \(t\) 次修改后的序列(第 \(t\) 个版本)。
在排序的时候,先按照左端点所在块由小到大排序,再按照右端点所在块由小到大排序(可以根据左端点所在块的编号进行奇偶性优化),再按照时间从小到大排序。
在用上一次的答案 \(Q_{i-1}\) 更新这一次答案 \(Q_i\) 的时候,像普通莫队一样,通过移动左右指针进行区间的增减,把上一次查询的区间位置 \([l_{i-1},r_{i-1}]\) 转换成 \([l_i,r_i]\) 。又因为上一次的查询基于第 \(t_{i-1}\) 个版本,还要移动时间轴,把序列更新为第 \(t\) 个版本。具体的更新方法就是直接在序列上依次执行 \(t_{i-1}\) 到 \(t_i\) 之间所有的修改操作,同时更新答案。
几何法理解带修莫队
还记得静态莫队的几何法理解吗?当时我们把每个询问 \([l,r]\) 看成了平面内一个点 \((l,r)\) 。现在加入了时间轴 \(t\) ,就可以把一个询问 \([l,r],t\) 看成三维空间内一个点 \((l,r,t)\) 。从原点出发,沿着坐标轴走(增减、更新序列),当走到点 \((l,r,t)\) 时,得到询问 \([l,r],t\) 的答案,一直走下去直到空间内所有点都走过,即得到所有询问的解。
复杂度证明
带修莫队的复杂度比较玄学,我也不太会证,这里写个大概,仅供参考。
设:块长为 \(L\) ,\(c\) 为修改数,\(q\) 为询问数,块指代询问(左端点)所在的块,询问为 \([l,r]\) 。
- 对于时间指针 \(t\) :左端点所在块相同时,右端点所在块单调递增,如果右端点相同,那么 \(t\) 递增,此时 \(t\) 最多移动 \(c\) 次。左端点相同的询问有 \(\frac n L\) 个,则这些询问中右端点所在块相同的有 \(\frac {n^2} {L^2}\) 个,总次数 \(\frac {n^2c} {L^2}\) ;
- 对于左指针 \(l\) :在左端点所在的块内移动,移动次数不超过 \(2L\) ,总次数 \(qL\) ;
- 对于右指针 \(r\) :当左端点所在块相同时,右端点所在块递增,最坏移动为 \(n\) 。一共有 \(\frac n L\) 个块,总次数 \(\frac {n^2} L\) ;
故所有指针的总移动复杂度是 \(O\left( \frac {n^2c}{L^2}+qL+\frac {n^2}{L} \right)\) 。
但是一般的题目不会告诉你具体多少次询问修改,所以统一用操作数 \(m\) 表示,即 \(O\left( \frac {n^2m}{L^2}+mL+\frac {n^2}{L} \right)\) 。
这里我们想要莫队跑的更快,操作空间就只有块长 \(L\) 。
那么 \(L\) 具体取多少呢......借助一些神奇的计算软件,我得到了这个式子:
emmm...... 还是不要纠结块长多少的好。视作 \(n,m\) 为同数量级,有 \(L=\sqrt[3]{n^2}\) 时取得渐进时间复杂度约为 \(O(\sqrt[3]{n^5})\) 。
所以在设定块长的时候可以 len=(int)pow(n,0.6666666666);
。
Part 2 带修莫队例题
带修莫队我目前没找到大量练习题目,只有这一道板子。这里挖个坑:以后如果遇到带修莫队的题目要在这里整理总结。
\(\text{Update 2021/7/12}\) :我来填坑,下去看 T2 !
T1 [国家集训队]数颜色
题目链接:Link
题目描述:
给你长度为 \(N\) 的序列 \(A\) ,有 \(m\) 次操作。
- 形如 Q L R 的指令,查询 \([L,R]\) 之间有多少个不同的元素。
- 形如 R P C 的指令,表示把 \(A_P\) 修改为 \(C\) 。
Solution:
这题和 HH 的项链那题非常像,就是多了一个修改操作,别的没了。
于是用带修莫队时间轴维护修改即可,注意代码实现与常数优化(否则你过不去这个板子)。
莫队由于本身效率算不上高,这里有一些卡常数小技巧:
-
每一条语句能精简就精简,不要使用过多的
if-else
语句嵌套,尽量使用三目运算符替代。语句中==
符号可以用异或x^x
替代(这个做法我不知道有没有用)。比如这一段(重载小于号运算符用来排序),上面的写法会比下面的写法快(尽管看上去是等价的)。
在同样评测环境下(C++11 标准,开启 O2 优化,luogu 评测机),第一种写法最大数据点仅仅运行 861ms,而第二种写法却会超时(运行时间大于 2700 ms)。
inline bool operator < (const Node a,const Node b){ return (bel[a.l]^bel[b.l]) ? bel[a.l]<bel[b.l] : ((bel[a.r]^bel[b.r]) ? ((bel[a.l]&1) ? bel[a.r]<bel[b.r]:bel[a.r]>bel[b.r]) : a.t < b.t); } /* inline bool operator < (const Node a,const Node b){ if(bel[a.l]!=bel[b.l]) return bel[a.l]<bel[b.l]; else if(bel[a.r]!=bel[a.r]){ if(bel[a.l]&1) return bel[a.r]<bel[b.r]; else return bel[a.r]>bel[b.r]; }else return a.t<b.t; } */ //即使上面那种写法比较阴间,但是你也得硬着头皮这么写!
-
注意奇偶性优化,这题我一开始没加奇偶性优化,TLE 到飞起(不过好像有人没开奇偶优化也过了)。
-
尽量少使用 STL 模板库中一些实现简单的函数,因为它会慢(但是它不像某些人所宣传的那样慢的骇人听闻)。比如这一段代码,我用到了交换也就是
std::swap()
函数。#define swap(x,y) x^=y,y^=x,x^=y; swap(x,y); /* #include<algorithm> std::swap(x,y); */
上面的
swap
是我宏定义的,而下面是算法库里自带的。在同样评测环境下(C++11 标准,开启 O2 优化,luogu 评测机),上面的写法最大数据点运行 861 ms,下面的写法最大数据点运行 931 ms(虽然差不了多少,但是能快一点是一点啊)。
Code:
感觉上面叙述了一大顿也没讲明白,那就看代码吧...
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>
//using namespace std;
const int maxn=140005;
#define swap(x, y) x ^= y, y ^= x, x^= y
// #define int long long
template <typename _T>
inline void read(_T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
x*=fh;
}
int n,m,len;
int A[maxn],bel[maxn];//A存序列,表示第i个元素属于bel[i]块
struct Node{
int l,r,t,org;
};
int Qnum;//询问总数
struct Node query[maxn];
inline bool operator < (const Node a,const Node b){
return (bel[a.l]^bel[b.l]) ? bel[a.l]<bel[b.l] : ((bel[a.r]^bel[b.r]) ? ((bel[a.l]&1) ? bel[a.r]<bel[b.r]:bel[a.r]>bel[b.r]) : a.t < b.t);
}//奇偶性优化
struct QAQ{
int pos,val;
};
int Mnum;//修改总数
struct QAQ modify[maxn];//存修改操作
int ans,cnt[1000005];//答案和用来更新它的桶
inline void add(int i){
ans+=!cnt[i]++;//阴间卡常操作
}
inline void del(int i){
ans-=!--cnt[i];
}
inline void change(const int now,const int i){
if(modify[now].pos >= query[i].l && modify[now].pos <=query[i].r)
del(A[modify[now].pos]),add(modify[now].val);//如果修改在这段询问区间内,那么要更新答案
swap(modify[now].val,A[modify[now].pos]);
//交换值,这里不能直接赋值,因为在之后的求解中有可能要把序列改回之前的某一个版本。
}
int ans1[maxn];
signed main(){
#ifdef WIN32
freopen("a.in", "r", stdin);
freopen("a.out","w",stdout);
#endif
read(n),read(m);
len=(int)pow(n,0.6666666666);//上面已证带修莫队最佳块长
for(int i=1;i<=n;++i){
bel[i]=i/len+1;
read(A[i]);
}
for(int i=1;i<=m;++i){
char opt[3];
scanf("%s",opt);
if(opt[0]=='Q'){
++Qnum;
read(query[Qnum].l);
read(query[Qnum].r);
query[Qnum].t=Mnum;
query[Qnum].org=Qnum;
}else{
++Mnum;
read(modify[Mnum].pos);
read(modify[Mnum].val);
}
}//读入所有操作
std::sort(query+1,query+Qnum+1);
int l=1,r=0,now=0;
for(int i=1;i<=Qnum;++i){
while(l<query[i].l) del(A[l++]);
while(l>query[i].l) add(A[--l]);
while(r<query[i].r) add(A[++r]);
while(r>query[i].r) del(A[r--]);
while(now>query[i].t) change(now--,i);//移动时间轴
while(now<query[i].t) change(++now,i);
ans1[query[i].org]=ans;
}
for(int i=1;i<=Qnum;++i)
printf("%d\n",ans1[i]);
return 0;
}
T2 CF940F
一道伪装成回滚莫队的带修莫队。
题目链接:Link
题目描述:
给定一个长度为 \(N\) 的序列 \(A\) ,要求支持两种操作:
- 查询区间 \([l,r]\) 中每种数字出现次数的 \(mex\) 值。
- 修改位置为 \(p\) 的元素。
\(mex\) 值指的是一个数集中最小的没有出现过的正整数。
Solution:
看过我的这篇博客或者做过有关求 \(mex\) 的题目的同学可能知道,一种很方便的求区间 \(mex\) 的方法是:回滚莫队。
但是这个题带修改,而带修莫队和回滚莫队又是水火不相容(如果要带修,因为 change 函数的关系,必须同时支持添加、删除两种操作,而这是回滚莫队无法做到的)。不用回滚莫队的话,删除操作之后就要疑似 \(O(N)\) 重新求 \(mex\) ,这样复杂度就不对...这个问题似乎无解?
没什么思路,我闲的没事做,先打了一发带修莫队暴力(就是直接修改、添加、删除,所有操作完之后直接重新统计答案),然后就... AC 了?
难道是数据水了?CF 的叉人机制不可能让暴力过去的,肯定是有什么性质我没发现。
注意到这个题和普通求 \(mex\) 的题不同,它是求元素出现次数的 \(mex\) 。可以设暴力从 1 开始枚举,最终答案是 \(k\) ,那么就枚举了 \(k-1\) 个数。而要用各个元素出现次数构成这 \(k-1\) 个数,至少用 \(\sum_{i=0}^{k-1} i\approx k^2\) 个数!我们一共才有 \(N\) 个数,那么 \(k\) 最大也就不过 \(\sqrt N\) 而已,也就是暴力的复杂度不超过 \(\sqrt N\) 。而带修莫队转移一次为 \(\sqrt[3] {N^2}\) 左右,这个复杂度完全可以接受。
Code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iostream>
#define int long long
#define swap(x, y) x ^= y, y ^= x, x^= y
const int maxn=200005;
template <typename _T>
inline _T const& read(_T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
return x*=fh;
}
int n,q,len,T;
int A[maxn],B[maxn];
int bel[maxn];
struct Node{
int l,r,t,org;
};
int Qnum;
struct Node query[maxn];
inline bool operator < (const Node a,const Node b){
return bel[a.l]^bel[b.l] ? bel[a.l] < bel[b.l] : (bel[a.r]^bel[b.r] ? (bel[a.l]&1 ? a.r<b.r : a.r>b.r) : a.t<b.t);//加上奇偶性优化
}
struct QAQ{
int pos,val;
};
int Mnum;
struct QAQ Modify[maxn];
int ans;
int cnt[maxn],tot[maxn];
inline void add(const int i){//添加值i
++cnt[i];
--tot[cnt[i]-1];//比如一个数出现2次->3次,不仅要把3次的桶+1,也要把2次的桶-1
++tot[cnt[i]];//这相当于数从一个桶跳到另一个桶去了
//反正最后都要暴力求解,这里就不必再更新答案了
}
inline void del(const int i){//删除值i
--cnt[i];
--tot[cnt[i]+1];
++tot[cnt[i]];//原因同上调整
}
void Getans(){//暴力求解答案
ans=1;
while(tot[ans]>0)
ans++;
}
inline void change(const int now,const int i){
if(query[i].l<=Modify[now].pos && Modify[now].pos<=query[i].r)
del(A[Modify[now].pos]),add(Modify[now].val);
swap(Modify[now].val,A[Modify[now].pos]);
}
void Init(){
read(n),read(q);
len=(int)pow(n,0.6666666666);
for(int i=1;i<=n;++i){
bel[i]=(i-1)/len+1;
B[++T]=read(A[i]);
}
for(int i=1,op;i<=q;++i){
read(op);
if(op==1){
++Qnum;
read(query[Qnum].l);
read(query[Qnum].r);
query[Qnum].t=Mnum;
query[Qnum].org=Qnum;
}else{
++Mnum;
read(Modify[Mnum].pos);
read(Modify[Mnum].val);
B[++T]=Modify[Mnum].val;//这里要一起离散化掉
}
}
std::sort(B+1,B+1+T);
int m=std::unique(B+1,B+1+T)-B-1;
for(int i=1;i<=n+Mnum;++i){//值域较大,要离散化(好押韵呀
if(i<=n) A[i]=std::lower_bound(B+1,B+m+1,A[i])-B;
else Modify[i-n].val=std::lower_bound(B+1,B+m+1,Modify[i-n].val)-B;
}
}
int ans1[maxn];
signed main(){
Init();
std::sort(query+1,query+1+Qnum);
int l=1,r=0,now=0;
for(int i=1;i<=Qnum;++i){
while(l<query[i].l) del(A[l++]);
while(l>query[i].l) add(A[--l]);
while(r<query[i].r) add(A[++r]);
while(r>query[i].r) del(A[r--]);
while(now<query[i].t) change(++now,i);
while(now>query[i].t) change(now--,i);//普通带修莫队
Getans();//暴力一发
ans1[query[i].org]=ans;
}
for(int i=1;i<=Qnum;++i)
printf("%lld\n",ans1[i]);
return 0;
}