莫队套值域分块
莫队套值域分块
在经典的“静态区间第 \(k\) 小”问题中,我们已经知道有可持久化线段树(主席树)做法和线段树套平衡树等做法。假设现在要求支持单点修改,变成“动态区间第 \(k\) 小”问题,线段树套平衡树仍然可做,主席树则需要在外侧套一个树状数组。这样总复杂度为 \(O(Nlog^2N)\) 。
但是众所周知,树套树死难写,一不小心就会造成“想题 5 分钟,写挂 2 小时”的惨案。今天介绍一种更简单的实现“动态区间 \(k\) 小”的做法——莫队套值域分块,也就是“块套块”。
Part 1 值域分块
值域分块,顾名思义是对序列 \(A\) 的值域 \((A_i\in [1,M])\) 进行分块。主要用途是维护一堆数,支持 \(O(1)\) 插入、删除,以及 \(O(\sqrt N)\) 复杂度实现查询前驱、后继、\(k\) 小、\(x\) 的排名(类似于平衡树)。
问题引入
在介绍值域分块之前,先思考这样一个问题:
- 如果让你用一个桶来维护一列正整数数中的第 \(k\) 小,你会怎么做?
一个显然的做法是把所有的数离散化后扔进桶里(这相当于桶排序),每次从 1(值域下界)开始枚举到值域上界 \(M\),找到出现的第 \(k\) 个数是几。假设有 \(N\) 次询问,那么这个算法的时间复杂度是 \(O(NM)\) 的,并不优秀。如果我们使用分块来维护这个桶,就可以把时间复杂度降低到 \(O(NT)\) ,其中 \(T\) 为块长。而这,就是所谓“值域分块”。
操作介绍
假设一个无序数列 \(A\) ,其中 \(A_i\in [1,M]\) (需离散化)。
-
预处理
对区间 \([1,M]\) 分块处理,设块长 \(s=T\) 。设桶 \(cnt[x]\) 表示数 \(x\) 出现的次数,另外块内再维护一个数组 \(num[x]\) ,表示第 \(x\) 块内一共有几个数(也就是 \(\sum_{l_x}^{r_x} cnt[i]\))。 然后暴力地把 \(A\) 中所有的数插入到桶 \(cnt\) 中,利用上面的式子计算出 \(num[i]\) 的值。
-
查询 \(k\) 小
因为把所有操作都讲一遍实在太复杂了,这里用查询 \(k\) 小举例子。
“问题引入”部分中提到过,查询“第 \(k\) 小”核心的算法是:从 1(值域下界)开始枚举到值域上界,桶中出现的第 \(k\) 个数就是答案。
现在从第一块开始一块一块地枚举,\(num[1]\) 表示值域区间 \([1,T]\) 中有几个数。如果 \(k\geq num[1]\) ,说明值在 \([1,T]\) 中的数不足 \(k\) 个,那么 \(k\) 小也就肯定不在第一块里。现在已经出现了 \(num[1]\) 个数了,我们一共要找 \(k\) 个,那么还要找 \(k-num[1]\) 个数,更新 \(k\) 的值,去下一块内找。下一块处理过程以此类推。
重复这个寻找过程,直到在第 \(i\) 块内,\(k\leq num[i]\) ,这说明 \(k\) 小一定出现在这个块内。
第 \(i\) 块代表的值域区间是 \([l_i,r_i]\) ,答案一定在区间 \([l_i,r_i]\) 里。从 \(l_i\) 枚举到 \(r_i\) 。利用刚才预处理出来的桶 \(cnt[x]\) 检查当前枚举到的数 \(x\) 是不是在序列中出现过,如果出现过,\(k\) 自减 \(cnt[x]\) 。
重复这个寻找过程,更新 \(k\) 的值,直到 \(k\leq 0\) 此时枚举到的数就是 \(k\) 小,即答案。
-
插入、删除
这个操作很简单,直接在桶中和这个数对应的块中增减出现次数即可。
-
时间复杂度
查询操作先 \(O(T)\) 枚举了块,然后在某个块内暴力枚举 \(k\) 小的大小(枚举不超过 \(T\) 次),这样查询总复杂度 \(O(T)\) 。
插入、删除操作显然是 \(O(1)\) 的,直接在对应数组(\(cnt[\ ],num[\ ]\))中增减即可。
但是这样写也有可能不对,值域分块的正确写法应该是块状链表。
如果一个块内出现的数字过多,时间复杂度很有可能退化到 \(O(N)\) ,这时需要块状链表的分裂操作。
然而块状链表太难写,况且离散化之后同一块内数字过多这种情况很难出现,所以直接写分块就好。
Part 2 莫队套值域分块
书接上回说到:值域分块可以解决序列上的一些问题,其具体功能类似于平衡树。注意到值域分块插入删除都是 \(O(1)\) 的,这和需要大量插入删除操作以维护区间信息的莫队简直是天作之合。
例1 经典区间第 \(k\) 小问题
- 您需要写一种数据结构,支持查询一段序列中给定区间 \([l,r]\) 中所有元素中第 \(k\) 小的数。
读者可能已经有思路了:用普通的莫队维护区间,问题间转移的时候 \(O(1)\) 增减值域分块中的元素。当莫队把已知区间移动到询问区间的时候,利用值域分块查询答案即可。
例2 二逼平衡树
例题 1 很简单对吧?现在看一个加强版的例题:
- 您需要写一种数据结构,来维护一个有序数列,其中需要提供以下操作:
- 查询 \(v\) 在区间 \([l,r]\) 中的排名。
- 查询区间 \([l,r]\) 中排第 \(k\) 名元素的值。
- 修改某个位置上的数值。
- 在区间 \([l,r]\) 内查询 \(k\) 的前驱(严格小于 \(k\) 且最大的数,若不存在输出 -2147483647)。
- 在区间 \([l,r]\) 内查询 \(k\) 的后继(严格大于 \(k\) 且最小的数,若不存在输出 2147483647)。
查询区间第 \(k\) 小、查排名、查前驱、查后继,如果读者已经理解了值域分块的原理,那么这些操作都不难实现。
等等...这个题还要求支持修改操作,怎么办?你怕不是忘了莫队可以带修啊?
好了恭喜您又双叒叕秒了一个题,用带修莫队套值域分块的办法,可以在 \(O(n^\frac 5 3 +n\sqrt n)\) 的时间内求解本题。
\(\text{Talk is cheap,shou you the code.}\)
需要注意的是,这个题的值域很大,需要离散化处理,特别是操作中的某些数也需要一起离散化掉。
ps:变量名在初始化函数中定义,先阅读 void Init()
函数有助于理解代码。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
namespace Fast_IO{
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*10+ch-'0';
ch=getchar();
}
return x*=fh;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
}
// namespace Fast_IO
using namespace Fast_IO;
//using namespace std;
const int maxn=100005;
const int maxm=10005;
#define ll long long
#define swap(x, y) x ^= y, y ^= x, x^= y
int n,m,tot,len,it;
int A[maxn],B[maxn*2];
int num[maxm],cnt[maxn];//num[i]表示第i块中数字的数量,cnt[i]表示i出现的数量
int bel[maxn],L[maxm],R[maxm];//bel[i]表示i属于第几块.L[i],R[i]表示第i块的左右端点
struct Node{
int opt,l,r,t,k,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];//记录询问
inline void add(const int i){
cnt[i]++;
num[bel[i]]++;
}
inline void del(const int i){
cnt[i]--;
num[bel[i]]--;
}//在桶中添加,删除元素,很简单的
inline void change(const int now,const int i){
if(query[i].l<=modify[now].pos && modify[now].pos<=query[i].r)
add(modify[now].val),del(A[modify[now].pos]);
swap(modify[now].val,A[modify[now].pos]);
}//带修莫队要更新维护时间轴
int get_rank(int k){//get rank of k in range[l,r]
int rank=1;
for(int i=1;i<=tot;++i){//找到值域上界
if(k<=R[i])//k不如这个块右端点大,那么k一定在这个块中
for(int j=L[i];j<k;++j)//暴力枚举到k,找到比k小的有几个书
rank+=cnt[j];
else rank+=num[i];//如果k比这个块中元素都大,那么答案加上这个块中的元素数量。
}
return rank;
}
int get_kth(int k){//get kth number in range[l,r]
for(int i=1;i<=tot;++i){//同上,枚举到值域上界
if(k<=num[i])//如果k出现次数不足这个块中的数的数量了,说明第k名在这个块内
for(int j=L[i];j<=R[i];++j){//枚举这个块中所有元素
k-=cnt[j];//每找到一个元素,k减去这个元素数量
if(k<=0) return B[j];//如果k小于0,说明当前元素就是第k名
}
else k-=num[i];//k不在这个块中,减掉这个块中数的数量,去下个块查
}
return -1;//gg,返回-1
}
/*
查k的前驱可以先查k的排名rank,然后查排名为rank-1的数的值
查k的后继同上
*/
int get_pre(const int k){
int rank=get_rank(k);
if(rank==1) return -2147483647;//k已经是第一名了,不存在前驱
int pre=get_kth(rank-1);//查排名前一位元素
return pre;
}
int get_back(const int k){
int rank=get_rank(k);
if(rank==-1) return 2147483647; //k最大,不存在答案
int back=cnt[k]>0?get_kth(rank+1):get_kth(rank);
//这里细节一波,因为查的数本身可能出现在序列里,故特判
if(back==-1) return 2147483647; //没查到后继,不存在答案
return back;
}
void Init(){
read(n),read(m);//n个元素,m个操作
//对原数组分块
len=pow(n,0.6666666666);
for(int i=1;i<=n;++i){
bel[i]=(i-1)/len+1;//预处理,第i个元素属于bel[i]块
B[++it]=read(A[i]);//准备离散化
}
//读入,对询问排序
for(int i=1,op;i<=m;++i){
read(op);
if(op==3){//replace A[pos] with val
Mnum++;
read(modify[Mnum].pos);
B[++it]=read(modify[Mnum].val);
}else{
Qnum++;
query[Qnum].opt=op,query[Qnum].t=Mnum;
//其实这里查排名为k的元素不能离散化,但是为了方便,一起加入离散化数组到时候再特判掉
read(query[Qnum].l),read(query[Qnum].r);
B[++it]=read(query[Qnum].k);
query[Qnum].org=Qnum;
}
}
std::sort(query+1,query+1+Qnum);//排序
//离散化
std::sort(B+1,B+1+it);
it=std::unique(B+1,B+1+it)-B-1;
for(int i=1;i<=n+Mnum+Qnum;++i){
if(i<=n) A[i]=std::lower_bound(B+1,B+it+1,A[i])-B;
else if(i<=n+Mnum) modify[i-n].val=std::lower_bound(B+1,B+it+1,modify[i-n].val)-B;
else if(query[i-n-Mnum].opt!=2) query[i-n-Mnum].k=std::lower_bound(B+1,B+it+1,query[i-n-Mnum].k)-B;//2操作是查排名为k的元素,这个k不能离散化,特判掉
}
//初始化值域分块
len=sqrt(it);//块长为sqrt(it)
tot=it/len;//总共tot个块
for(int i=1;i<=it;++i)
bel[i]=(i-1)/len+1;//重定义bel[i]表示值为i的元素属于值域分块的bel[i]块
for(int i=1;i<=tot;++i){
if(i*len>it) break;
L[i]=(i-1)*len+1;
R[i]=i*len;//预处理每一块的左右端点
}
if(R[tot]<it)
tot++,L[tot]=R[tot-1]+1,R[tot]=it;
}
int ans1[maxn];
signed main(){
Init();
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);//正常带修莫队操作
if(query[i].opt==1)
ans1[query[i].org]=get_rank(query[i].k);
else if(query[i].opt==2)
ans1[query[i].org]=get_kth(query[i].k);
else if(query[i].opt==4)
ans1[query[i].org]=get_pre(query[i].k);
else if(query[i].opt==5)
ans1[query[i].org]=get_back(query[i].k);//按 要 求 回 答 问 题
}
for(int i=1;i<=Qnum;++i)
printf("%d\n",ans1[i]);//输出答案
return 0;
}
例3 [AHOI]2013 作业
其实本来例 3 不是这道题,但是由于原来准备当例 3 的那个题理解起来简直让人逝世(各种数组连环嵌套),所以临时决定换这道题。正好也是省选题目,质量应该也比某谷月赛题目好罢。
题目链接:Link
题目描述:
给一个长度为 \(n(n\leq 10^5)\) 的正整数数列。
\(m(m\leq 10^5)\) 次操作,每次给定 \(l,r,a,b\) ,要求输出大小在 \([a,b]\) 之间的数的个数,以及符合条件的数有几种。
Solution:
经典莫队+值域分块的题目。查询 \([a,b]\) 中的数时,先暴力 \(a,b\) 所在段元素,然后整段处理。如果 \(a,b\) 在同一段直接暴力。另外对于第二问再开一个桶分别统计答案即可,具体看代码。
Code:
由于上面的注释比较详细了,这里代码不再加注。望理解。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
namespace IO{
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*10+ch-'0';
ch=getchar();
}
return x*=fh;
}
inline void write(long long a){
if(a>=10) write(a/10);
putchar(a%10+'0');
}
} // namespace IO
using namespace IO;
//using namespace std;
const int maxn=100005;
const int maxm=2005;
int n,m,len,tot;
int A[maxn],bel[maxn];//*对A[i]值域分块
int cnt[maxn];
int L[maxm],R[maxm],num[maxm],val[maxm];//num[i]表示第i块内的数字个数,val[i]表示第i块内的数字种类
struct Node{
int l,r,a,b,org;
};
struct Node query[maxn];
bool operator < (const Node a,const Node b){
return bel[a.l]^bel[b.l]?bel[a.l]<bel[b.l]:(bel[a.l]&1?a.r<b.r:a.r>b.r);
}
inline void add(const int i){
++num[bel[A[i]]];
++cnt[A[i]];
if(cnt[A[i]]==1) val[bel[A[i]]]++;
}
inline void del(const int i){
--num[bel[A[i]]];
--cnt[A[i]];
if(cnt[A[i]]==0) val[bel[A[i]]]--;
}
int ans,kind;
inline void get_num(int a,int b){
ans=0;kind=0;
if(bel[a]==bel[b]){
for(int i=a;i<=b;++i)
ans+=cnt[i],kind+=cnt[i]>0;
return;
}
for(int i=a;i<=R[bel[a]];++i)
ans+=cnt[i],kind+=cnt[i]>0;
for(int i=bel[a]+1;i<=bel[b]-1;++i)
ans+=num[i],kind+=val[i];
for(int i=L[bel[b]];i<=b;++i)
ans+=cnt[i],kind+=cnt[i]>0;
}
void Init(){
read(n),read(m);
len=sqrt(n);
tot=n/len;
for(int i=1;i<=tot;++i){
if(i*len>n) break;
L[i]=(i-1)*len+1;
R[i]=i*len;
}
if(R[tot]<n)
tot++,L[tot]=R[tot-1]+1,R[tot]=n;
for(int i=1;i<=n;++i){
bel[i]=(i-1)/len+1;
read(A[i]);
}
for(int i=1;i<=m;++i)
read(query[i].l),read(query[i].r),read(query[i].a),read(query[i].b),query[i].org=i;
std::sort(query+1,query+1+m);
}
std::pair<int,int> ans1[maxn];
signed main(){
Init();
int l=1,r=0;
for(int i=1;i<=m;++i){
while(l<query[i].l) del(l++);
while(l>query[i].l) add(--l);
while(r<query[i].r) add(++r);
while(r>query[i].r) del(r--);
get_num(query[i].a,query[i].b);
ans1[query[i].org].first=ans;
ans1[query[i].org].second=kind;
}
for(int i=1;i<=m;++i)
printf("%d %d\n",ans1[i].first,ans1[i].second);
return 0;
}