整体二分
整体二分是一个求解区间第K小(大)非常优秀的算法,但是要求离线处理,对于所有询问做整体的二分答案操作。相较于主席树 ,树套树,整体二分( 应该 )更加优秀。
我用主席树与整体二分写,并没有发现在时间上整体二分快多少,我自己算时间复杂度也觉得两者差不多(也可能我写的太丑了),但是空间上整体二分当然非常占优
上问题: (对于整体二分带修改的其实和不带修改其实差不多,后面会说)
给定一个长度为n数列,进行m次询问,每次询问 l 到 r 的 第 k 小的数是多少。
首先,我们考虑怎样对一个询问进行二分答案求解,从-inf到inf不断二分答案,每一次二分后,在数列中求出所询问区间内小于等于mid的个数num,如果num>=k,说明在这num个小于等于mid的数当中包含所求答案,反之答案则在区间大于mid的数当中,这时,更新数列为原数列中小于等于mid数,或者其他大于mid的数,便于下次二分答案。
而求解区间内有多少个小于等于mid的数时,单次询问当然直接遍历,但是在整体二分时,面对许多询问,我们可以用树状数组,把数列中小于等于mid的数的位置下标标记,然后sum(r) - sum(l-1)求出此区间小于等于mid的数。为了方便大家理解,我下面对于单组询问也做树状数组的处理
整体二分即是把这种二分答案操作做出整体把握,对所有询问二分,所以一定要理解这种求解方式
下面我给出模拟过程:
首先一个长度为7数列: 6(1) 2(2) 5(3) 3(4) 1(5) 9(6) 2(7) 询问 2 到 6 的第 3 小数,括号里是位置下标
an:a1 a2 …an 数列
l :二分答案的左区间
r :二分答案的右区间
L:询问区间的左区间
R:询问区间的右区间
K: 第几小
因为数列中最小为1,最大为9,我令 l = 1 , r = 9;
首先初始状态:
an: 6(1) 2(2) 5(3) 3(4) 1(5) 9(6) 2(7)
l = 1 , r = 9 , L = 2 , R = 6 , K = 3
1:
mid = (l+r)>>1 = 5;
插入 2(2) 5(3) 3(4) 1(5) 2(7) 的位置到树状数组。
查询 num = sum® - sum(L-1) = 4;
发现 num >= k , 说明答案小于等于5;
所以 令 r = mid;
并更新数列为: 2(2) 5(3) 3(4) 1(5) 2(7)
2:
mid = (l+r)>>1 = 3;
插入 2(2) 3(4) 1(5) 2(7) 的位置到树状数组。
查询 num = sum® - sum(L-1) = 3 ;
发现 num >= k , 说明答案小于等于3;
所以 令 r = mid;
并更新数列为: 2(2) 3(4) 1(5) 2(7)
3:
mid = (l+r)>>1 = 2;
插入 2(2) 1(5) 2(7) 的位置到树状数组。
查询 num = sum® - sum(L-1) = 2 ;
发现 num < k , 说明答案大于3;
所以 令 l = mid+1;K = K - 2;
并更新数列为: 3(4)
4:
l = r = 3 , 所以答案为3;
整体二分则是在上述的基础上,把询问打包存下来,每次先遍历数列,插入小于等于mid的数的位置,再处理所有询问,如果该询问的答案小于等于mid,就对应到小于等于的mid的新数列里,反之对应另一边,这样不断整体分划,直到l==r时,此时对应的某些询问,答案就都是l。
还是上代码,理解了上面,代码多看几遍应该就OK了
区间第K小
#include<iostream>
using namespace std;
const int maxn=1e5+5 , maxm=1e4 , inf=1e9+7;
int n,m,cnt;
int ans[maxn];
struct node{
int x,y,k;
int pos,tp;
};
node z[maxn+maxm],le[maxn+maxm],ri[maxn+maxm];
int sum[maxn];
int lowbit(int x){ return x&(-x); }
void add(int x,int d){
while(x<=n){
sum[x] += d; x += lowbit(x);
}
}
int query(int x){
int ans = 0;
while(x>0){
ans += sum[x]; x -= lowbit(x);
}
return ans;
}
void CDQ(int l,int r,int L,int R)
{
if(l>r||L>R) return;
if(l==r){
for(int i=L;i<=R;i++)
if(z[i].tp) ans[z[i].pos] = l;
return;
}
int mid = (l+r)>>1;
int cntl = 0 , cntr = 0;
for(int i=L;i<=R;i++){
if(z[i].tp){ // 询问
int tem = query(z[i].y) - query(z[i].x-1);
if(tem<z[i].k) z[i].k -= tem , ri[++cntr] = z[i];
else le[++cntl] = z[i];
}
else{ // 插入
if(z[i].x<=mid) add(z[i].pos,1) , le[++cntl] = z[i];
else ri[++cntr] = z[i];
}
}
for(int i=1;i<=cntl;i++) if(!le[i].tp) add(le[i].pos,-1); // 取消标记,使树状数组清空,方便下次操作
for(int i=1;i<=cntl;i++) z[L+i-1] = le[i]; // 以mid为界限分划为两个新整体,数列与询问都分开了
for(int i=1;i<=cntr;i++) z[L+cntl+i-1] = ri[i];
CDQ(l,mid,L,L+cntl-1);
CDQ(mid+1,r,L+cntl,R);
}
int main()
{
ios::sync_with_stdio(false);
cin>>n>>m;
cnt = 0;
for(int i=1;i<=n;i++){ // 先放原数列,因为先插入到树状数组后才能查询
int a; cin>>a;
z[++cnt] = {a,1,0,i,0}; // z[i].tp==0 表示插入操作
}
for(int i=1;i<=m;i++){ // 后放询问
int a,b,c; cin>>a>>b>>c;
z[++cnt] = {a,b,c,i,1}; // z[i].tp==1 表示询问
}
CDQ(-inf,inf,1,cnt);
for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
}
修改其实就只是比不修改的多了一个删除,树状数组中取消标记就好了
我直接上个需要修改的代码,与上面的大致一样
区间第K小带修改
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e4+5 , maxm=1e4+5 , inf=1e9+7;
int n,m,va[maxn],ans[maxm];
struct node{
int l,r,k,pos,tp;
};
node z[maxn*2],zl[maxn*2],zr[maxn*2];
int C[maxn];
void add(int x,int d){
while(x<=n) { C[x] += d , x += (x&-x); }
}
int sum(int x){
int s = 0;
while(x) { s += C[x] , x -= (x&-x); }
return s;
}
void CDQ(int l,int r,int L,int R)
{
if( l>r || L>R ) return;
if(l==r){
for(int i=L;i<=R;i++)
if(z[i].tp==3) ans[z[i].pos] = l;
return;
}
int mid = (l+r)>>1;
int p = 0, q = 0;
for(int i=L;i<=R;i++){
if(z[i].tp==1){
if(z[i].k<=mid) add(z[i].pos,1), zl[++p] = z[i]; // 插入
else zr[++q] = z[i];
}
else if(z[i].tp==2){
if(z[i].k<=mid) add(z[i].pos,-1), zl[++p] = z[i]; // 删除
else zr[++q] = z[i];
}
else { // 询问
int tem = sum(z[i].r) - sum(z[i].l-1);
if(z[i].k<=tem) zl[++p] = z[i];
else z[i].k -= tem , zr[++q] = z[i];
}
}
for(int i=1;i<=p;i++){ // 取消标记
if(zl[i].tp==1) add(zl[i].pos,-1);
if(zl[i].tp==2) add(zl[i].pos,1);
}
for(int i=1;i<=p;i++) z[L+i-1] = zl[i];
for(int i=1;i<=q;i++) z[L+p+i-1] = zr[i];
CDQ(l,mid,L,L+p-1);
CDQ(mid+1,r,L+p,R);
}
int main()
{
ios::sync_with_stdio(false);
int t; cin>>t;
while(t--){
int ki = 0,cnt=0,a,b,c;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>va[i];
z[++ki] = (node){ 0,0,va[i],i,1 }; // z[i].tp==1 表示插入
}
char d;
for(int i=1;i<=m;i++){
cin>>d;
if(d=='Q'){
cin>>a>>b>>c;
z[++ki] = (node){ a,b,c,++cnt,3 }; // z[i].tp==3 表示询问
}
else {
cin>>a>>b;
z[++ki] = (node){ 0,0,va[a],a,2 }; // z[i].tp==2 表示删除
z[++ki] = (node){ 0,0,b,a,1 }; // 先删除原位置,再添加新位置
va[a] = b;
}
}
CDQ(0,inf,1,ki);
for(int i=1;i<=cnt;i++)
cout<<ans[i]<<endl;
}
}
不想拿金的Acmer不是好Acmer
问题 详解&spm=1001.2101.3001.5002&articleId=96170510&d=1&t=3&u=9302765badfb436a8693e5e9869deca7)
1447

被折叠的 条评论
为什么被折叠?



