[ZJOI2013]K大数查询 浅谈整体二分
题目大意
重新讲一下含糊不清的题意:
有n个可重集合,有m个操作,操作分为两种:
- 1 l r c 给第l到第r个可重集合都加入一个数c。
- 2 l r c 询问第l到第r个可重集合第c大的数是多少。
\(n\le 50000,m\le 50000,1\le l\le r\le n\),1中的\(|c|\le n\),2中的\(c\le long\space long\)
sol
暴力
给每一个点开一个vector
暴力插入,询问时将l到r的数暴力排一个序。时间复杂度:\(\Theta(n^3\log n)\)。
基于暴力,我们可以用树套树来做。
二分
对与每个询问,我们可以在-n到n之间二分,二分出一个mid后,我们暴力扫一遍所有在它之前的插入,统计出l到r中有多少个数大于mid,如果大于mid的数大于c,说明我们二分小了,反之说明我们二分大了。
时间复杂度:\(\Theta(n^2\log n)\)
整体二分
整体二分顾名思义就是所有的询问一起二分。其实在单次二分时,我们得出了很多有用信息,对之后的询问依然有用,所以我们可把所有询问一起二分。
二分思路就在上面。下面讲一些不一样的地方。
我们先新开出一个区间和两个数组,第一个数组存要向小二分的操作,第二个数组存要向大二分的操作。
在整体二分时,我们二分出了一个mid,依次扫过每一个操作:
- 插入,讨论它的c值,如果大于mid,就在新区间的l到r加一,把它扔到第二个数组,否则扔到第一个数组。
- 查询,令num等于新区间l到r的值之和,则num就等于l到r大于mid的数的个数。如果num小于c,则c-=num,把操作扔到第一个数组里,否则扔到第二个数组。
最后,递归处理两个数组。
证明一下这样做的正确性:
查询时,如果c大于num,那么对num产生贡献的插入都会和它到不同的数组中,以后不会再产生贡献。但由于我们往小二分,那些插入实际上是会对它产生贡献的,所以我们现在就要把贡献记下来,实际操作就是减去num。如果c小于num,那么对num产生贡献的插入都会和它到相同的数组中,没有问题。
插入操作其实是随着查询操作来的,查询没错它就没错。
对于新区间的区间修改,区间查询,树状数组或线段树即可。
时间复杂度:\(\Theta(n\log^2 n)\)
还有一些细节和优化都在代码注释里。
code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=50010;
int n,m;
ll ans[maxn];
struct BIT {//树状数组
ll c[maxn];
inline void add(int x,int y) {
for(int i=x; i<=n; i+=i&-i)c[i]+=y;
}
inline ll sum(int x) {
ll ans=0;
for(int i=x; i; i-=i&-i)ans+=c[i];
return ans;
}
} a,b,c;
struct node {//操作的结构体
int tp,l,r,id;
ll c;
} q[maxn],tem1[maxn],tem2[maxn];//tem为两新数组
void solve(int x,int y,int l,int r) {//整体二分
if(l==r) {//找到答案
for(int i=x; i<=y; i++)
ans[q[i].id]=l;
return;
}
int mid=(l+r)>>1,cnt1=0,cnt2=0;
for(int i=x; i<=y; i++)
if(q[i].tp==2) {//查询
ll num=q[i].r*a.sum(q[i].r)-b.sum(q[i].r)-(q[i].l-1)*a.sum(q[i].l-1)+b.sum(q[i].l-1);
if(num<q[i].c) {
q[i].c-=num;
tem1[++cnt1]=q[i];
} else tem2[++cnt2]=q[i];
} else {//插入
if(q[i].c>mid) {
a.add(q[i].l,1),a.add(q[i].r+1,-1);
b.add(q[i].l,q[i].l-1),b.add(q[i].r+1,-q[i].r);
tem2[++cnt2]=q[i];
} else tem1[++cnt1]=q[i];
}
for(int i=x; i<=y; i++)//清空树状数组,不能用memset,否则时间会爆炸
if(q[i].tp==1&&q[i].c>mid) {
a.add(q[i].l,-1),a.add(q[i].r+1,1);//照样减回去
b.add(q[i].l,-q[i].l+1),b.add(q[i].r+1,q[i].r);
}
for(int i=1; i<=cnt1; i++)q[x+i-1]=tem1[i];//滚动一下优化空间
for(int i=1; i<=cnt2; i++)q[x+cnt1+i-1]=tem2[i];
if(cnt1)solve(x,x+cnt1-1,l,mid);//分别去二分
if(cnt2)solve(x+cnt1,y,mid+1,r);
}
int main() {
scanf("%d%d",&n,&m);
int Q=0;
for(int i=1; i<=m; i++) {
scanf("%d%d%d%lld",&q[i].tp,&q[i].l,&q[i].r,&q[i].c);
if(q[i].tp==2)q[i].id=++Q;
}
solve(1,m,-n,n);
for(int i=1; i<=Q; i++)
printf("%lld\n",ans[i]);
return 0;
}
总结
整体二分不能在线,时间复杂度其实和树套树一样。但它代码短,常数小,还是挺有用的一个算法。