bzoj3110 K大数查询
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©<=N
2操作中c<=Maxlongint
解题思路:
百度一下,会发现大部分题解都是用树套树来做的,有的是用线段树套线段树,有的用树状数组套树状数组。但本题实际上也可以用二分的策略来解决。我们用二分+树状数组来解决该题。前置知识:二分、树状数组(区间修改)。
首先明晰目标:求l到r位置上所有数(每个位置上可能不止一个数)第c大的数。
我们假设所有操作(或询问,或插入)都在操作集合que中。而用一个长整型数组ans来存储答案,用一个num数组来存放所有要插入的c(num数组中所有元素唯一,即unique一下)。
每次递归我们取出一个中间数num[mid],如果当前操作que[i]是插入操作,我们判断其c是否大于等num[mid],若大于等于则对que[i]所对应的[l,r]区间所有位置+1。那么树状数组每个位置存放的就是该位置>num[mid]的元素的个数(为啥这样?因为我们求的是第k大的数,是降序!)。所以当查询时,若此时区间[l,r]内数的个数 >= 第k大(实际上是c,但为了区别就叫它c,下同)数,则说明num[mid]就可能是第k大嘛。(比num[mid]大的数假设有cnt个,而我们要求第k大的数,若 cnt >= k ,那么至少第k大的数包含在这cnt个数中) 那我们下面要做的就是缩小范围,直至确定第k大的数是谁。
所以我们分两部分递归,一部分是小于num[mid]的small派,另一部分是已经有粗略答案的big派。对于small派,我们目标是减小num[mid]来找寻其可能的答案;对于big派,我们目标是增大num[mid]来精确其答案。
代码示例:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const int MAXN = 60000;
struct Query{
/*
op为操作类型
l和r分别为左右边界
c见题意
cnt为二分时累加的值
aidx为ans数组的下标
*/
int op,l,r,aidx;
ll c,cnt;
Query(){}
Query(int op,int l,int r,ll c):op(op),l(l),r(r),c(c){
cnt = 0;
}
bool operator < (const Query &B) const{
return c < B.c;
}
};
//下面三个数组分别是:操作集合、归并用临时数组1,临时数组2
Query que[MAXN],tmp1[MAXN],tmp2[MAXN];
ll ans[MAXN]; //答案数组
vector<ll> num; //用来存不同的c,便于二分
//下面是树状数组代码
ll st[2][MAXN];
void add(int k,int x,int y){
for(;x < MAXN;x += x&-x) st[k][x] += y;
}
ll ask(int k,int x){
ll res = 0;
for(;x;x -= x&-x) res += st[k][x];
return res;
}
//将que中下标为id的元素l,r所有位置+1
void superAdd(int id){
add(0,que[id].l,1);
add(0,que[id].r+1,-1);
add(1,que[id].l,que[id].l);
add(1,que[id].r+1,-que[id].r-1);
}
//返回que中下标为id的[l,r]区间内元素个数
ll superAsk(int id){
ll res = (que[id].r+1)*ask(0,que[id].r) - ask(1,que[id].r);
res -= (que[id].l)*ask(0,que[id].l-1) - ask(1,que[id].l-1);
return res;
}
void clear(int id){
add(0,que[id].l,-1);
add(0,que[id].r+1,1);
add(1,que[id].l,-que[id].l);
add(1,que[id].r+1,que[id].r+1);
}
//线段树区间修改代码到此结束
void solve(int ql,int qr,int l,int r){
/*
其中ql、ql分别是操作集合que的左右端点,请注意,左闭右开
l和r是num数组的左右端点,左闭右开
*/
int mid = (l+r)/2;
int bcnt = 0,scnt = 0;
//bigcnt,smallcnt,分别代表大于num[mid]的和小于num[mid]的元素
for(int i = ql;i < qr;i++){
if(que[i].op == 1){
if(que[i].c >= num[mid]) superAdd(i),tmp1[bcnt++] = que[i];
else tmp2[scnt++] = que[i];
}else{
if(que[i].cnt + superAsk(i) >= que[i].c){
ans[que[i].aidx] = num[mid];
tmp1[bcnt++] = que[i];
}else{
que[i].cnt += superAsk(i);
tmp2[scnt++] = que[i];
}
}
}
for(int i = ql;i < qr;i++)
if(que[i].op == 1 && que[i].c >= num[mid])
clear(i);
for(int i = 0;i < scnt;i++) que[ql+i] = tmp2[i];
for(int i = 0;i < bcnt;i++) que[ql+scnt+i] = tmp1[i];
/*
要在这里递归和判断是否满足条件,因为本次操作对下一次操作有影响,
所以需要先操作,再递归;而防止死循环需要如下判断条件
*/
if(ql >= qr-1 || l >= r-1) return;
solve(ql,ql+scnt,l,mid);
//大的集合也要递归,因为可能答案会小于num[mid]呢,相当不断逼近答案
solve(ql+scnt,qr,mid,r);
}
int main(){
int n,m;
int acnt = 0; //答案数组的大小
scanf("%d%d",&n,&m);
for(int i = 0;i < m;i++){
int op,l,r;
ll c;
scanf("%d%d%d%lld",&op,&l,&r,&c);
que[i] = Query(op,l,r,c);
if(op == 1) num.push_back(c);
else que[i].aidx = acnt++;
}
sort(num.begin(),num.end());
int sz = unique(num.begin(),num.end()) - num.begin();
solve(0,m,0,sz); //左闭右开
for(int i = 0;i < acnt;i++)
printf("%lld\n",ans[i]);
return 0;
}