BZOJ3110 ZJOI2013 K大数查询
3110: [Zjoi2013]K大数查询
Time Limit: 20 Sec Memory Limit: 512 MB
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
1 1 2 1
1 1 2 2
2 1 1 2
2 1 1 1
2 1 2 3
Sample Output
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,a<=b<=N
1操作中abs(c)<=N
2操作中c<=Maxlongint
为什么BZOJ的排版这么鬼里鬼气。
一道整体二分的入门题?反正我是做了蛮久的。
都说了是整体二分了,那就分一下吧。
自我感觉,整体二分是这么一个东西:
二分答案都会,对于每组询问,二分答案再check看是否满足。
那么整体二分,就是对所有询问加操作一起二分。
二分的同样是答案,然后询问就可以分成两部分了。
同时把操作也分成两部分。
一部分是对前面询问要产生贡献的,一部分是已经计算完对后面的询问贡献的(不会改变/不会影响的)。
然后两部分递归求解。
以区间k大为例,若有多个询问,可以整体二分。
把比二分答案mid大的数加1,check就看区间和是否大于k。
递归求解,直到答案l==r为止。
所以整体二分大概就长成这样:
solve(optl,optr,l,r){
if(optl>optr)return;
if(l==r){
for(int i=optl;i<=optr;++i)
if(opt[i]是询问)
Ans[opt[i].id]=l;
return;
}
……
solve(optl,optl+tot1-1,l,mid);
solve(optl+tot1,optr,mid+1,r);
}
中间判断一般就是树状数组?线段树?差分?……
按照XHR犇所说,整体二分复杂度要和操作序列的长度线性相关。
也就是说当前长度是len那么复杂度就要和len线性相关,不然破坏复杂度。
回头来看这道题。
答案明显是满足可二分性的。那么试着整体二分。
按照套路,solve(optl,optr,l,r)。
搞出一个mid,来看看怎么check。
对于加边操作,如果c>mid,就区间+1。
然后这个操作就丢进右边队列。
如果c<=mid,则对答案无贡献,丢进左边。
对于询问,就是查询区间和sum。
如果sum<k,意味着第k大一定不比mid大,丢进左边。
否则k-=sum,丢进右边(想想主席树的操作)。
然后就是递归处理了。
对于上面那个数据结构,树状数组肯定比线段树好是吧......
我还去%了一波Me2O3学姐的树状数组区间修改......顺便A了codevs的线段树板子。
HoHo。
然后就把板子压进结构体我会说
#include <iostream> #include <cstdio> #include <cstdlib> #include <algorithm> #include <vector> #include <cstring> #include <queue> #include <complex> #include <stack> #define LL long long int #define dob double using namespace std; const int N = 100010; struct Data{ int type,l,r,c,id; bool operator <(const Data &a)const{ return id<a.id; } }opt[N],que1[N],que2[N]; int n,m,tim;LL Ans[N]; struct BIT{ LL A1[N],A2[N],A3[N]; int vis1[N],vis2[N],vis3[N]; inline int lb(int k){ return k&-k; } inline void update(LL *A,int *vis,int x,int val){ for(;x<=n;x+=lb(x)){ if(vis[x]!=tim)A[x]=0; A[x]+=val;vis[x]=tim; } } inline LL query(LL *A,int *vis,int x,LL ans=0){ for(;x;x-=lb(x)){ if(vis[x]!=tim)A[x]=0; ans+=A[x];vis[x]=tim; } return ans; } inline void add(int l,int r,int dt){ update(A2,vis2,l,dt);update(A2,vis2,r+1,-dt); update(A3,vis3,l,dt*l);update(A3,vis3,r+1,-dt*(r+1)); } inline LL sum(int l,int r){ LL sl=A1[l-1]+l*query(A2,vis2,l-1)-query(A3,vis3,l-1); LL sr=A1[r]+(r+1)*query(A2,vis2,r)-query(A3,vis3,r); return sr-sl; } }T; inline int gi(){ int x=0,res=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')res*=-1;ch=getchar();} while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar(); return x*res; } /* 整体二分? 二分答案c,求: solve(optl,optr,ansl,ansr); 即work出操作在[ol,or],答案在[al,ar]? 先搞出一个答案mid,再把opt扫一边。 如果是1 如果c>mid,就丢进树状数组里面。 区间+1? 三个树状数组的事情。 然后把操作扔进右边QwQ 如果c<=mid 就不管它 然后把操作丢进左边。 如果是2 查询一下和。 如果sum<k 则意味着前面的操作里凑不出k个比mid大的数。 则第k大一定比mid小。 丢进左边。 否则 丢进右边。 然后 solve(optl,opt1+sizel-1,ansl,mid); solve(optr-size2+1,optr,mid+1,ansr); 直到l==r为止。 */ inline void solve(int optl,int optr,int l,int r){ if(optl>optr)return;++tim; if(l==r){ for(int i=optl;i<=optr;++i) if(opt[i].type==2) Ans[opt[i].id]=l; return; } int mid=((l+r+2*n)>>1)-n,tot1=0,tot2=0; for(int i=optl;i<=optr;++i){ if(opt[i].type==1){ if(opt[i].c>mid){ T.add(opt[i].l,opt[i].r,1); que2[++tot2]=opt[i]; } else que1[++tot1]=opt[i]; } else{ LL sum=T.sum(opt[i].l,opt[i].r); if(sum<opt[i].c){ opt[i].c-=sum; que1[++tot1]=opt[i]; } else que2[++tot2]=opt[i]; } } int k=optl; for(int i=1;i<=tot1;++i)opt[k++]=que1[i]; for(int i=1;i<=tot2;++i)opt[k++]=que2[i]; solve(optl,optl+tot1-1,l,mid); solve(optr-tot2+1,optr,mid+1,r); } int main() { freopen("K大数查询.in","r",stdin); freopen("K大数查询.out","w",stdout); n=gi();m=gi(); for(int i=1;i<=m;++i){ opt[i].type=gi(); opt[i].l=gi();opt[i].r=gi(); opt[i].c=gi();opt[i].id=i; } solve(1,m,-n,n);sort(opt+1,opt+m+1); for(int i=1;i<=m;++i) if(opt[i].type==2) printf("%lld\n",Ans[i]); fclose(stdin);fclose(stdout); return 0; }