[ZJOI2013]K大数查询 浅谈整体二分

题目大意

题面链接:
bzoj3110
洛谷P3332

重新讲一下含糊不清的题意:

有n个可重集合,有m个操作,操作分为两种:

  • 1 l r c 给第l到第r个可重集合都加入一个数c。
  • 2 l r c 询问第l到第r个可重集合第c大的数是多少。

(nle 50000,mle 50000,1le lle rle n),1中的(|c|le n),2中的(cle longspace long)

sol

暴力

给每一个点开一个vector暴力插入,询问时将l到r的数暴力排一个序。时间复杂度:(Theta(n^3log n))

基于暴力,我们可以用树套树来做。

二分

对与每个询问,我们可以在-n到n之间二分,二分出一个mid后,我们暴力扫一遍所有在它之前的插入,统计出l到r中有多少个数大于mid,如果大于mid的数大于c,说明我们二分小了,反之说明我们二分大了。

时间复杂度:(Theta(n^2log n))

整体二分

整体二分顾名思义就是所有的询问一起二分。其实在单次二分时,我们得出了很多有用信息,对之后的询问依然有用,所以我们可把所有询问一起二分。

二分思路就在上面。下面讲一些不一样的地方。

我们先新开出一个区间和两个数组,第一个数组存要向小二分的操作,第二个数组存要向大二分的操作。

在整体二分时,我们二分出了一个mid,依次扫过每一个操作:

  1. 插入,讨论它的c值,如果大于mid,就在新区间的l到r加一,把它扔到第二个数组,否则扔到第一个数组。
  2. 查询,令num等于新区间l到r的值之和,则num就等于l到r大于mid的数的个数。如果num小于c,则c-=num,把操作扔到第一个数组里,否则扔到第二个数组。

最后,递归处理两个数组。

证明一下这样做的正确性:

查询时,如果c大于num,那么对num产生贡献的插入都会和它到不同的数组中,以后不会再产生贡献。但由于我们往小二分,那些插入实际上是会对它产生贡献的,所以我们现在就要把贡献记下来,实际操作就是减去num。如果c小于num,那么对num产生贡献的插入都会和它到相同的数组中,没有问题。

插入操作其实是随着查询操作来的,查询没错它就没错。

对于新区间的区间修改,区间查询,树状数组或线段树即可。

时间复杂度:(Theta(nlog^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
",ans[i]);
	return 0;
}

总结

整体二分不能在线,时间复杂度其实和树套树一样。但它代码短,常数小,还是挺有用的一个算法。

原文地址:https://www.cnblogs.com/hht2005/p/11421235.html