BZOJ 3110 [Zjoi2013]K大数查询(整体二分+树状数组)

3110: [Zjoi2013]K大数查询

Time Limit: 20 Sec   Memory Limit: 512 MB
Submit: 9615   Solved: 2842
[ Submit][ Status][ Discuss]

Description

有N个位置,M个操作。操作有两种,每次操作如果是1 a b c的形式表示在第a个位置到第b个位置,每个位置加入一个数c
如果是2 a b c形式,表示询问从第a个位置到第b个位置,第C大的数是多少。

Input

第一行N,M
接下来M行,每行形如1 a b c或2 a b c

Output

输出每个询问的结果

Sample Input

2 5
1 1 2 1
1 1 2 2
2 1 1 2
2 1 1 1
2 1 2 3

Sample Output

1
2
1

HINT



【样例说明】

第一个操作 后位置 1 的数只有 1 , 位置 2 的数也只有 1 。 第二个操作 后位置 1

的数有 1 、 2 ,位置 2 的数也有 1 、 2 。 第三次询问 位置 1 到位置 1 第 2 大的数 是

1 。 第四次询问 位置 1 到位置 1 第 1 大的数是 2 。 第五次询问 位置 1 到位置 2 第 3

大的数是 1 。‍


N,M<=50000,N,M<=50000

a<=b<=N

1操作中abs(c)<=N

2操作中c<=Maxlongint



        其实呢,这题和之前写过的那个动态区间第K大很类似,只不过那个是修改,而这个是插入,但是同样都可以用线段树套Treap来实现。但是今天要说的是另外一种更加巧妙的分治方法,使得编程复杂度大幅下降——整体二分。

        首先,我们知道,如果仅仅只是询问区间第K大,我们用的是二分答案。具体做法是二分一个数字,然后看区间内有多少个数字比他大,如果恰好有K-1个,那么枚举到的这个数字就是区间第K大。对于这道题目,我们同样也可以运用这种思想,只不过我们还要把这个所有的操作也和这个二分答案的过程一起做,所以顾名思义这个方法叫做整体二分。我定义solve(1,m,1,n)表示解决区间[1,m]的操作,然后他们的答案在区间[1,n]中。

        类似与二分答案,我要动态的统计大于mid的数字的出现次数。于是从第一个操作开始,如果是插入操作,而且插入的数字大于mid,那么把执行此操作并把这个操作丢入操作队列B中,否则不执行,操作丢入操作队列A中;如果是查询操作,那么我们就查询对应区间大于mid的数字数量,如果大于K,那么说明mid偏小了,再把它丢入操作队列B中,如果小于说明mid偏大了,K减去大于mid的数量之后再把这个操作丢入操作队列A中。执行完一遍之后,我们再来看看操作队列A和操作队列B中分别是什么。操作队列A中含有的是插入数字小于等于mid的插入操作,和假设以mid为第K大但是发现比他大的数字小于K-1的查询操作;操作队列B中是插入数字大于mid的插入操作,和假设以mid为第K大但是发现比他大的数字大于K-1的查询操作。可以看出,经过这样一个整体二分的过程,我们把所有操作分成了两个部分,而且两个部分内又是各自有着时间的顺序。我们假设操作序列A的长度为lenA,那么我们接下来要做的就是solve(1,lenA,1,mid)和solve(lenA+1,m,mid+1,n)。

        精髓在于,先分再治。把操作和答案一起二分,然后二分的同时把操作和答案都分成两个部分,然后在分别治。然后在处理的过程中,我们需要一个支持区间修改,区间查询的数据结构,这里我用了树状数组,顺便补充一下模板,毕竟这个非常的短小精悍。然后又涉及到一个清零的问题,因为每次治完之后树状数组需要进行清零,这里有两种方式,一是打一个标签,实际上是时间戳,如果时间戳不同,那么在查询和修改的时候首先单个清零;另一种是在治完之后,重现便利操作,把之前更新的减去。为了方便我选择了后者,但是在常数上可能会更慢。值得注意的是,树状数组要使用LL,毕竟加的数字可能比较多。具体见代码:

#include<bits/stdc++.h>
#define lowbit(x) x&-x
#define LL long long
#define N 100010

using namespace std;

struct node
{
    int op,l,r;
    LL k;int id;
} p[N],tmp[2][N];

int n,m,ans[N];

struct EX_BIT										//支持区间修改、区间查询的树状数组
{
    struct binaryIndexTree
    {
        LL c[N];
        void init(){memset(c,0,sizeof(c));}
        void update(int x,LL k){for(;x<=n;c[x]+=k,x+=lowbit(x));}
        LL sum(int x){LL ans=0;for(;x>0;ans+=c[x],x-=lowbit(x));return ans;}
    } BIT1,BIT2;

    void init(){BIT1.init();BIT2.init();}
    LL getsum(int l,int r){return sum(r)-sum(l-1);}
    void update(int l,int r,LL x){add(l,x);add(r+1,-x);}
    LL sum(LL x){return (x+1)*BIT1.sum(x)-BIT2.sum(x);}
    void add(int x,LL k){BIT1.update(x,k);BIT2.update(x,x*k);}
} BIT;

void solve(int L,int R,int l,int r)						//整体二分
{
    if (L>R) return;
    if (l==r)										//如果到了单位答案区间,那么答案确定了
    {
        for(int i=L;i<=R;i++)
            if (p[i].op==2) ans[p[i].id]=l;
        return;
    }
    int t1=0,t2=0,mid=(l+r)>>1;
    for(int i=L;i<=R;i++)
    {
        if (p[i].op==1)
        {
            if (p[i].k<=mid) tmp[0][t1++]=p[i];					//对于插入操作,分为大于mid和小于等于mid两种情况
            else tmp[1][t2++]=p[i],BIT.update(p[i].l,p[i].r,1);
        } else
        {
            LL temp=BIT.getsum(p[i].l,p[i].r);
            if (temp<p[i].k) p[i].k-=temp,tmp[0][t1++]=p[i];			//查询操作则类似与普通二分答案,看数量是否大于K
                                     else tmp[1][t2++]=p[i];
        }
    }
    int Mid=L+t1;
    for(int i=L;i<Mid;i++)								//临时数组数据转移
    {
        p[i]=tmp[0][i-L];
        if (p[i].op==1&&p[i].k>mid) BIT.update(p[i].l,p[i].r,-1);
    }
    for(int i=Mid;i<=R;i++)
    {
        p[i]=tmp[1][i-Mid];
        if (p[i].op==1&&p[i].k>mid) BIT.update(p[i].l,p[i].r,-1);
    }
    solve(L,Mid-1,l,mid); solve(Mid,R,mid+1,r);					//分开成两部分去治
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int op,l,r,k;
        scanf("%d%d%d%d",&op,&l,&r,&k);
        p[i]=node{op,l,r,k,i};
    }
    solve(1,m,1,n);
    for(int i=1;i<=m;i++)
        if (ans[i]!=0) printf("%d\n",ans[i]);
    return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值