整体二分\cdq分治——洛谷P3332 [ZJOI2013]K大数查询
https://daniu.luogu.org/problem/show?pid=3332
第一次接触整体二分;
上课的时候小红说这用树套树做,但感觉好难啊,二维线段树都不会,怎么做树套树啊;
然后就去做整体二分了;
整体二分通过递归实现,和线段树一样,把一段东西变成两段,处理好这两段之间的关系后,这两段就相对独立了,我们直接分治这两段;
首先这题我们要离线做;
总共有两种操作
插入,查询;
查询的是第k大;
我们二分这个答案;
二分区间就是k的取值范围,即-n~n;
二分出来的中间值mid;
对于插入;
设插入的值是v;
如果v<=x;那么现在来说这个v对答案没什么贡献;
因为我们要求比mid大的有几个;
那么就直接把这个操作放到左堆;
分治左堆的时候二分的区间会减小,右对会增大;
就是说当前的堆是l~r
做对l~mid;
右堆mid+1~r;
如果v>mid;
显然如果执行这个操作那么会对答案照成贡献;
那么我们用线段树记录一下这个贡献;
树状数组不会
记录好之后这个操作就无用了,放到右堆;
对于查询;
设查询区间xl,xr;
查询第k大;
我们看看xl~xr这个区间现在值是v;
v代表xl~xr区间里面比mid大的数的个数;
这个就是上面说到的贡献,用线段树取维护;
如果v < k,
那么我们直接把k-v之后把这个操作放到左堆;
因为mid+1~r的区间的值只有v个,k>v所以显然答案在l~mid里面;
要是v>=k直接放到右堆分治;
整个思想就是二分里面套分治,再加一颗线段树;
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#define ui unsigned int
using namespace std;
struct in{
int num,l,r,kind,v,ans,c;
}a[50005];
struct tree{
ui v,tag;
bool p;
}T[131072];
int n,m;
bool cmp2(in x,in y){return x.num<y.num;}
bool cmp1(in x,in y){return x.c<y.c;}
void pushdown(int num,int l,int r){
if(l==r)return;
if(T[num].p){//更新儿子;
T[num*2].v=T[num*2+1].v=0;
T[num*2].tag=T[num*2+1].tag=0;
T[num*2].p=T[num*2+1].p=1;
T[num].p=0;
}
int mid=(l+r)/2;
T[num*2].v+=(mid-l+1)*T[num].tag;
T[num*2+1].v+=(r-(mid+1)+1)*T[num].tag;
T[num*2].tag+=T[num].tag;
T[num*2+1].tag+=T[num].tag;
T[num].tag=0;
}
void init(int l,int r,int x,int y,int num){
pushdown(num,l,r);
if(x<=l&&r<=y){
T[num].v+=(r-l+1)*1;
T[num].tag+=1;
return;
}
int mid=l+r>>1;
if(mid >=x)init(l,mid ,x,y,num<<1 );
if(mid+1<=y)init(mid+1,r,x,y,num<<1|1);
T[num].v=T[num*2].v+T[num*2+1].v;
}
ui outit(int l,int r,int x,int y,int num){
pushdown(num,l,r);
if(x<=l&&r<=y)return T[num].v;
int mid=l+r>>1;
ui ans=0;
if(mid >=x)ans+=outit(l,mid ,x,y,num<<1 );
if(mid+1<=y)ans+=outit(mid+1,r,x,y,num<<1|1);
return ans;
}
void er(int l,int r,int x,int y){
if(l==r){
for(int i=x;i<=y;i++)a[i].ans=l;//更新答案;
return;
}
int mid=l+r>>1;
int L=0,R=y;//分开左右堆
T[1].v=T[1].tag=0; T[1].p=1;//更新线段树
//p表示现在这个节点的左右儿子有没有被清空过,避免memset;
for(int i=x;i<=y;i++)
if(a[i].kind==1){
if(a[i].v<=mid)a[i].c=++L;else{
a[i].c=++R;
init(1,n,a[i].l,a[i].r,1);
}
}else{
ui temp=outit(1,n,a[i].l,a[i].r,1);
if(temp<(ui)a[i].v){
a[i].v-=temp;
a[i].c=++L;
}else a[i].c=++R;
}
sort(a+x,a+y+1,cmp1);//排好序左右对就分离了
er(l,mid,x,x+L-1);
er(mid+1,r,x+L,y);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&a[i].kind,&a[i].l,&a[i].r,&a[i].v);
a[i].num=i;
}
er(-n,n,1,m);//开始分治
sort(a+1,a+m+1,cmp2);
for(int i=1;i<=m;i++)
if(a[i].kind==2)printf("%d\n",a[i].ans);
}
然后我的同昔日好像全是树套树;
我也贴一下他们的代码吧;
有意思的;
外面是一个权值线段树;
里面一区间线段树;
每次在l~r区间里插一个v 的值;
对于外层线段树包括v的值的节点,在这个节点的内部区间线段树l~r区间+1;
然后查答案直接暴力二分;
这个和整体二分输出答案是一样的思想;
而且这段代码用了标记永久化;
感觉就是一个小优化吧;
蛮显然的;
还有树套树,能非递归就非递归吧;
看别人的代码蛮容易,自己写估计萎
#include<bits/stdc++.h>
using namespace std;
typedef unsigned int in;
inline in read(){
in k=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){k=k*10+ch-'0';ch=getchar();}
return k*f;
}
in root[200001],t[10000001],lt[10000001],rt[10000001],add[10000001],cnt=0;
in n,m,x,y,z;
inline void nxg(in l,in r,in& nod){
if(!nod)nod=++cnt;
if(l>=x&&r<=y){add[nod]++;t[nod]+=r-l+1;return;}
in mid=(l+r)>>1;
if(x<=mid)nxg(l,mid,lt[nod]);
if(y>mid)nxg(mid+1,r,rt[nod]);
t[nod]=t[lt[nod]]+t[rt[nod]]+add[nod]*(r-l+1);
}
inline void xg(in l,in r,in nod){
while(1){//非常和谐地变成了非递归
nxg(1,n,root[nod]);
if(l==r)return;
in mid=(l+r)>>1;
if(z<=mid){r=mid;nod*=2;continue;}
l=mid+1;(nod*=2)++;
}
}
inline in ns(in l,in r,in nod){
if(!nod)return 0;
if(l>=x&&r<=y)return t[nod];
in mid=(l+r)>>1,sum=0;
if(x<=mid)sum+=ns(l,mid,lt[nod]);
if(y>mid)sum+=ns(mid+1,r,rt[nod]);
return sum+add[nod]*(min(y,r)-max(l,x)+1);
}
inline in s(in l,in r,in nod){
while(1){
if(l==r)return l;
in mid=(l+r)>>1,cmp=ns(1,n,root[nod*2]);
if(z<=cmp){r=mid;nod*=2;continue;}
z-=cmp;l=mid+1;(nod*=2)++;
}
}
int main()
{
n=read();m=read();
for(in i=1;i<=m;i++){
in c=read();x=read();y=read();z=read();
if(c==1)z=n-z+1,xg(1,n,1);//先反一下数值,处理负数,再换成第k小做
else printf("%d\n",n-s(1,n,1)+1);
}
return 0;
}