介绍
整体二分是一种将所有修改和查询放在一起二分的离线算法。通过将所有操作二分,给每个查询查找一个正确的答案。
举个例子,求区间第 k k k小值。如果没有修改操作,那么用主席树就可以解决问题。但如果有修改操作的话,主席树就要套上一个数状数组,打起来十分麻烦。所以,对于这样的动态区间第 k k k小查询,我们就要用到整体二分了。
首先,我们将原数组的 n n n个值看作 n n n次插入操作,将修改操作看作一次删除操作和一次插入操作,那么所有操作都可以转化为插入、删除、查询操作。将这些操作按时间发生顺序放入队列中。这些是预处理操作。
之后开始整体二分。定义函数 g t ( h e a d , t a i l , l , r ) gt(head,tail,l,r) gt(head,tail,l,r)表示对于队列中 h e a d head head到 t a i l tail tail的操作,将所有查询操作赋上答案,保证这段操作中的所有查询操作的答案都在 [ l , r ] [l,r] [l,r]中。
对于 g t gt gt函数,如果 l = = r l==r l==r,那么队列中 h e a d head head到 t a i l tail tail的所有查询操作答案都为 l l l。否则:
- 用数状数组维护插入和删除操作,到查询操作时,在数状数组内求这段中有多少值,设为 t m p i tmp_i tmpi
- 再枚举一次,如果是查询操作,若该操作原有值加上 t m p i tmp_i tmpi小于等于目标值 k k k则放在第一类,否则放在第二类;如果是插入或删除操作,判断操作的位置,若操作位置小于等于 m i d mid mid,则放在第一类,否则放在第二类
- 第一类 g t gt gt一次,第二类 g t gt gt一次
设数列长为 n n n,操作数为 q q q,则总时间复杂度为 O ( ( n + q ) l o g 2 ( n + q ) ) O((n+q)log^2(n+q)) O((n+q)log2(n+q))
整体二分的思想与
c
d
q
cdq
cdq二分类似。因为十分巧妙,所以原理肯定很难看明白,建议根据代码来理解。我就是看了代码才理解的
主函数
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
w[++tot]=(node){i,a[i],0,1,0,0};
}
for(int i=1,x,y,z;i<=q;i++){
ch=getchar();
while(ch!='C'&&ch!='Q') ch=getchar();
if(ch=='Q'){
scanf("%d%d%d",&x,&y,&z);
w[++tot]=(node){x,y,z,3,++qt,0};
}
else{
scanf("%d%d",&x,&y);
w[++tot]=(node){x,a[x],0,2,0,0};
a[x]=y;
w[++tot]=(node){x,a[x],0,1,0,0};
}
}
gt(1,tot,0,inf);
for(int i=1;i<=qt;i++){
printf("%d\n",ans[i]);
}
}
其中结构体 w w w存储操作。 t p tp tp表示这是插入、删除、查询中的哪一类操作。
如果是插入或删除: x x x表示操作的节点, y y y表示要插入或删除的值
如果是查询: x , y x,y x,y表示要查询的区间 [ x , y ] [x,y] [x,y], z z z表示要查询的是第几小值, i d id id表示这是第几次查询, n o w now now表示目前有多少个数在前面
a a a值要在每次修改操作中更新
gt函数
void gt(int h,int t,int l,int r){
if(h>t) return;
if(l==r){
for(int i=h;i<=t;i++)
if(w[i].tp==3) ans[w[i].id]=l;
return;
}
int mid=(l+r)/2;
for(int i=h;i<=t;i++){
if(w[i].tp==1&&w[i].y<=mid) add(w[i].x,1);
else if(w[i].tp==2&&w[i].y<=mid) add(w[i].x,-1);
else if(w[i].tp==3) v[i]=find(w[i].y)-find(w[i].x-1);
}
for(int i=h;i<=t;i++){
if(w[i].tp==1&&w[i].y<=mid) add(w[i].x,-1);
else if(w[i].tp==2&&w[i].y<=mid) add(w[i].x,1);
}//不能用memset
int lt=0,rt=0;
for(int i=h;i<=t;i++){
if(w[i].tp==3){
if(w[i].now+v[i]>=w[i].z) w1[++lt]=w[i];
else{
w[i].now+=v[i];w2[++rt]=w[i];
}
}
else{
if(w[i].y<=mid) w1[++lt]=w[i];
else w2[++rt]=w[i];
}
}
for(int i=1;i<=lt;i++) w[h+i-1]=w1[i];
for(int i=1;i<=rt;i++) w[h+lt+i-1]=w2[i];
gt(h,h+lt-1,l,mid);gt(h+lt,t,mid+1,r);
}
注:数状数组清空不能用 m e m s e t memset memset,否则时间复杂度会大很多
例题
code
#include<bits/stdc++.h>
using namespace std;
int n,q,tot,qt,inf=1000000000,a[100005],v[500005],tr[500005],ans[100005];
char ch;
struct node{
int x,y,z,tp,id,now;
}w[500005],w1[500005],w2[500005];
int lb(int i){
return i&(-i);
}
void add(int i,int p){
while(i<=n){
tr[i]+=p;i+=lb(i);
}
}
int find(int i){
int re=0;
while(i){
re+=tr[i];i-=lb(i);
}
return re;
}
void dd(int h,int t,int l,int r){
if(h>t) return;
if(l==r){
for(int i=h;i<=t;i++)
if(w[i].tp==3) ans[w[i].id]=l;
return;
}
int mid=(l+r)/2;
for(int i=h;i<=t;i++){
if(w[i].tp==1&&w[i].y<=mid) add(w[i].x,1);
else if(w[i].tp==2&&w[i].y<=mid) add(w[i].x,-1);
else if(w[i].tp==3) v[i]=find(w[i].y)-find(w[i].x-1);
}
for(int i=h;i<=t;i++){
if(w[i].tp==1&&w[i].y<=mid) add(w[i].x,-1);
else if(w[i].tp==2&&w[i].y<=mid) add(w[i].x,1);
}
int lt=0,rt=0;
for(int i=h;i<=t;i++){
if(w[i].tp==3){
if(w[i].now+v[i]>=w[i].z) w1[++lt]=w[i];
else{
w[i].now+=v[i];w2[++rt]=w[i];
}
}
else{
if(w[i].y<=mid) w1[++lt]=w[i];
else w2[++rt]=w[i];
}
}
for(int i=1;i<=lt;i++) w[h+i-1]=w1[i];
for(int i=1;i<=rt;i++) w[h+lt+i-1]=w2[i];
dd(h,h+lt-1,l,mid);dd(h+lt,t,mid+1,r);
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
w[++tot]=(node){i,a[i],0,1,0,0};
}
for(int i=1,x,y,z;i<=q;i++){
ch=getchar();
while(ch!='C'&&ch!='Q') ch=getchar();
if(ch=='Q'){
scanf("%d%d%d",&x,&y,&z);
w[++tot]=(node){x,y,z,3,++qt,0};
}
else{
scanf("%d%d",&x,&y);
w[++tot]=(node){x,a[x],0,2,0,0};
a[x]=y;
w[++tot]=(node){x,a[x],0,1,0,0};
}
}
dd(1,tot,0,inf);
for(int i=1;i<=qt;i++){
printf("%d\n",ans[i]);
}
return 0;
}