整体二分学习笔记

整体二分,就是把查询放到一起二分,从而降低代码复杂度的一种离线算法。所以想要使用整体二分需要保证查询的答案具有单调性和操作可以离线

首先看一道例题:

有一个空的可重集和 mm 个操作,每个操作有类型 tpeitpe_i 和一个参数 xix_itpei=1tpe_i=1 的操作表示往可重集里添加 xix_itpe=i2tpe=_i2 的操作表示询问可重集内第 xix_i 小的数。

1m1051\le m\le 10^5

显然这道题可以用平衡树做,但是它太难写了。观察到答案具有单调性,所以可以二分答案,用一个数据结构维护当前可重集内小于 midmid 的数的个数即可。这样的时间复杂度是 O(mlogm)O(m\log m) 的,但是仍然很难写。

考虑降低代码复杂度。我们可以先把所有操作离线下来,然后对于所有操作一起二分“一起二分”就是将答案在 [l,r][l,r] 区间内的询问和对这些询问的答案有影响的修改操作一起处理curcur 表示 [l,r][l,r] 内的操作序列,那么显然若 l=rl=r,那么我们就已经找到答案了

考虑 l=rl\not=r 的情况。mid=l+r2mid=\left\lfloor\dfrac{l+r}{2}\right\rfloor,那么我们需要把 curcur 分为两个操作序列 lftlftrigrig,其中 lftlft 的答案在 [l,mid][l,mid] 内,rigrig 的答案在 [mid+1,r][mid+1,r] 内,这样我们递归下去就可以求出答案了

考虑怎么分组。对于修改操作 ii,显然若 ximidx_i \le mid 那么这个操作会对 lftlftrigrig 造成影响,但为了保证时间复杂度,我们只将它放入 lftlft 中;否则只会对 rigrig 造成影响,放入 rigrig

而对于询问操作 ii我们可以计算出操作序列执行到 ii 时可重集内比 midmid 小的数的个数 kk。若 kxik\ge x_i,那么说明当前 midmid 大了或者对了,这个询问操作需要分到 lftlft 里;否则说明当前 midmid 小了,这个询问操作需要分到 rigrig但是注意,k<xik<x_iii 被分到 rigrig 时,需要将 xix_i 减去 kk。因为我们加入修改操作时把所有对两个操作序列都有影响的操作放进了 lftlft 而没有加入 rigrig,所以我们需要额外计算上这些修改的贡献。

容易发现,计算操作序列执行到 ii 时可重集内比 midmid 小的数的个数 kk 可以用一个变量 cntcnt 简单统计。然后递归的层数是 logm\log m,每一层会遍历一次所有操作,所以时间复杂度为 O(mlogm)O(m\log m)

考虑如何求第 kk 大值。显然可以只有分组部分有变化:

对于修改操作 ii,显然若 xi>midx_i > mid 那么这个操作会对 lftlftrigrig 造成影响,但为了保证时间复杂度,我们只将它放入 rigrig 中;否则只会对 lftlft 造成影响,放入 lftlft

而对于询问操作 ii,我们可以计算出操作序列执行到 ii 时可重集内比 midmid 的数的个数 kkk>=xik>=x_i,那么说明当前 midmid 小了,这个询问操作需要分到 rigrig 里;否则说明当前 midmid 大了或者对了,这个询问操作需要分到 lftlft。但是注意,k<xik<x_i ii 被分到 lftlft,需要将 xix_i 减去 kk。因为我们加入修改操作时把所有对两个操作序列都有影响的操作放进了 rigrig 而没有加入 lftlft,所以我们需要额外计算上这些修改的贡献。

那么现在来看道例题吧:

P3332 [ZJOI2013]K大数查询

显然还是分组过程变了。发现变动的地方只是单点修改,整段查询变成了区间修改,区间查询。所以用线段树维护 [l,r][l,r] 内可重集的并内比 midmid 大的数的个数 kk 即可。

代码如下:

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

const long long MS=50005;

struct node
{
	int tpe,l,r,id;
	long long val;
};

int n,m,quecnt;
int ans[MS];
long long sum[MS<<2],lazy[MS<<2];

inline void updata(int u)
{
	sum[u]=sum[u<<1]+sum[u<<1|1];
}

inline void lazydown(int u,int l,int r)
{
	if(lazy[u]==0)
	{
		return;
	}
	int mid=l+r>>1;
	lazy[u<<1]+=lazy[u];
	lazy[u<<1|1]+=lazy[u];
	sum[u<<1]+=lazy[u]*(mid-l+1);
	sum[u<<1|1]+=lazy[u]*(r-mid);
	lazy[u]=0;
}

void upd(int u,int l,int r,int L,int R,long long k)
{
	if(r<L||l>R)
	{
		return;
	}
	if(l>=L&&r<=R)
	{
		lazy[u]+=k;
		sum[u]+=k*(r-l+1);
		return;
	}
	lazydown(u,l,r);
	int mid=l+r>>1;
	if(L<=mid)
	{
		upd(u<<1,l,mid,L,R,k);
	}
	if(R>=mid+1)
	{
		upd(u<<1|1,mid+1,r,L,R,k);
	}
	updata(u); 
}

long long que(int u,int l,int r,int L,int R)
{
	if(r<L||l>R)
	{
		return 0;
	}
	if(l>=L&&r<=R)
	{
		return sum[u];
	}
	lazydown(u,l,r);
	int mid=l+r>>1;
	long long res=0;
	if(L<=mid)
	{
		res+=que(u<<1,l,mid,L,R);
	}
	if(R>=mid+1)
	{
		res+=que(u<<1|1,mid+1,r,L,R);
	}
	return res;
}

void slove(int l,int r,vector<node> &cur)
{
	if(l==r)
	{
		for(node x:cur)
		{
			if(x.tpe==2)
			{
				ans[x.id]=l;
			}
		}
		return;
	}
	int mid=l+r>>1;
	vector<node> lft,rig;
	for(node x:cur)
	{
		if(x.tpe==1)
		{
			if(x.val<=mid)
			{
				lft.push_back(x);
			}
			else
			{
				rig.push_back(x);
				upd(1,1,n,x.l,x.r,1);
			}
		}
		else
		{
			long long vl=que(1,1,n,x.l,x.r);
			if(vl>=x.val)
			{
				rig.push_back(x);
			}
			else
			{
				x.val-=vl;
				lft.push_back(x);
			}
		}
	}
	for(node x:rig)
	{
		if(x.tpe==1)
		{
			upd(1,1,n,x.l,x.r,-1);
		}
	}
	if(lft.size()>0)
	{
		slove(l,mid,lft);
	}
	if(rig.size()>0)
	{
		slove(mid+1,r,rig);
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	vector<node> a;
	for(int i=1;i<=m;i++)
	{
		node x;
		scanf("%d%d%d%lld",&x.tpe,&x.l,&x.r,&x.val);
		if(x.tpe==2)
		{
			x.id=++quecnt;
		}
		a.push_back(x);
	}
	slove(-n,n,a);
	for(int i=1;i<=quecnt;i++)
	{
		printf("%d\n",ans[i]);
	}
	return 0;
}

练习题目