整体二分\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;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值