「学习笔记」整体二分

本文详细介绍了整体二分算法在数列查询、静态区间第k小、可重集合合并、矩阵乘法以及陨石收集问题等场景的应用。通过实例分析和代码展示,阐述了整体二分如何降低时间复杂度,优化解决方案,并提供了相关习题和参考资料,帮助读者深入理解和掌握这一算法。
摘要由CSDN通过智能技术生成

整体二分

应用前提

  1. 询问的答案具有可二分性。
  2. 修改对判定答案的贡献互相独立,修改之间互不影响效果。
  3. 修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值。
  4. 贡献满足交换律、结合律,具有可加性。
  5. 题目允许离线算法

引入1

在一个数列中查询第 k k k 小的数。

法1:简单粗暴,直接 sort,或者用 nth_element。

法2:考虑值域上的二分。用数据结构记录每个大小范围中有多少数,二分查找到位置。

引入2

在一个数列中多次查询第 k k k 小的数。

法1:简单粗暴,直接 sort。因为这既是静态,查询区间也不变。

法2:对每个询问采用上题的法2,分别进行二分。

法3:采用整体二分的思想。我们可以将所有的询问放在一起二分。

我们可以猜测当前所有询问的答案为 m i d mid mid,然后依次检验每个询问的答案,并依此分为小于等于和大于 m i d mid mid 两部分,对于两部分继续二分。这其实是一个分治的过程。

若询问的答案 ≤ m i d \le mid mid,则说明 m i d mid mid 是第 ≥ k \ge k k 小的数,因此第 k k k 小的数在 [ l , m i d ] [l,mid] [l,mid]

若询问的答案 > m i d >mid >mid,则说明 m i d mid mid 是第 < k <k <k 小的数,因此第 k k k 小的数在 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]。设询问得到 ≤ m i d \le mid mid 的数有 x x x 个,那么问题转化为求出现在值域 [ m i d + 1 , r ] [mid+1,r] [mid+1,r] 中第 k − x k-x kx 小的数。

l = r l=r l=r 时,我们结束该部分的二分,并给出该部分询问对应的答案。

下面贴个代码:

void solve(int l,int r,int L,int R)
{
	if(l==r)
	{
		for(int i=L;i<=R;i++)
			Ans[q[i].id]=l;
		/*值为l答案统计现场*/
		return;
	}
	int mid=(l+r)>>1;
	int cnt1=0,cnt2=0;
	for(int i=L;i<=R;i++)
	{
		int sum=query(mid);
		/*值域内查找<=mid的个数,一般用树状数组统计*/
		if(q[i].k<=sum)
			ql[++cnt1]=q[i];
		/*若<=说明在[l,mid]*/ 
		else
		{
			q[i].k-=sum;
			qr[++cnt2]=q[i];
		}
		/*否则说明在[mid+1,r]*/
	}
	/*由于接下来的二分我们还需要用到q数组
	所以需要将新处理的序列复制回去 
	*/
	for(int i=1;i<=cnt1;i++)
		q[i+L-1]=ql[i];
	/*复制左序列*/
	for(int i=1;i<=cnt2;i++)
		q[i+L+cnt1-1]=qr[i];
	/*复制右序列*/
	solve(l,mid,L,L+cnt1-1);
	solve(mid+1,r,L+cnt1,R);
}

引入3

静态区间第 k k k​ 小。

题目链接

我们曾经用主席树的写法解决了这个问题,现在用整体二分的想法去思考一下。

用类似于上题的写法,设当前询问为 q [ i ] q[i] q[i],我们需要统计出在 [ q [ i ] . l , q [ i ] . r ] [q[i].l,q[i].r] [q[i].l,q[i].r] 区间内 ≤ m \le m m 的个数。单点加,区间询问,用树状数组即可轻松解决。

注意,如果直接 memset 清空树状数组,会清空很多根本就没有操作过的位置。相反,我们只需将修改过的地方撤销操作即可。(即加上 -1

由于序列中原来已有初始值,为了能够顺利使用整体二分,我们将原序列的 n n n​ 个数看做是 n n n​ 次单点插入,将这些插入操作先于询问操作加入操作队列,这样就可以保证在查询前,对应区间内所需的所有插入已插入完毕。

具体时间复杂度分析及证明可以看这篇博客:https://blog.csdn.net/lwt36/article/details/50669972

void solve(int l,int r,int L,int R)
{
	if(l==r)
	{
		for(int i=L;i<=R;i++)
			if(q[i].op==2)Ans[q[i].id]=l;
		/*值为l答案统计现场*/
		return;
	}
	int mid=(l+r)>>1;
	int cnt1=0,cnt2=0;
	for(int i=L;i<=R;i++)
	{
		/*注意此处:
		如果在主函数内先添加修改操作,再添加查询操作
		那么修改操作一定先于其后的查询操作 
		因此两个操作可以合在一起写:
		*/
		if(q[i].op==1)
		{
			if(q[i].k<=mid)
			{
				ql[++cnt1]=q[i];
				add(q[i].id,1);
				/*在k这个值出现的位置id进行修改*/
			}
			else qr[++cnt2]=q[i]; 
		}
		else
		{
			int sum=query(q[i].r)-query(q[i].l-1);
			/*区间内查找<=mid的个数*/
			if(q[i].k<=sum)
				ql[++cnt1]=q[i];
			/*若<=说明在[l,mid]*/ 
			else
			{
				q[i].k-=sum;
				qr[++cnt2]=q[i];
			}
			/*否则说明在[mid+1,r]*/
		}
	}
	/*
	注意到如果即是修改又在ql数组内
	必然是添加操作
	直接用add(-1)的方法撤销即可 
	*/ 
	for(int i=1;i<=cnt1;i++)
		if(ql[i].op==1)
			add(ql[i].id,-1);
	/*由于接下来的二分我们还需要用到q数组
	所以需要将新处理的序列复制回去 
	*/
	for(int i=1;i<=cnt1;i++)
		q[i+L-1]=ql[i];
	/*复制左序列*/
	for(int i=1;i<=cnt2;i++)
		q[i+L+cnt1-1]=qr[i];
	/*复制右序列*/
	solve(l,mid,L,L+cnt1-1);
	solve(mid+1,r,L+cnt1,R);
}
/*
一组数据供模拟 
input:
5 5
3 1 2 4 5
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1
output:
1
2
4
3
4
*/

引入4

给定数列,支持单点修改,区间查询第 k k k​ 小。

题目链接

将单点修改改为:删去原数,在原位置插入新数。将一个操作拆为两个,几乎与引入3代码相同地套上整体二分即可。

具体代码实现

例题 1

【ZJOI2013】K大数查询

你需要维护 n n n 个可重整数集,集合的编号从 1 1 1 n n n
这些集合初始都是空集,有 m m m 个操作:

  • 1 l r k:表示将 k k k 加入到编号在 [ l , r ] [l,r] [l,r] 内的集合中。 ( 1 ≤ k ≤ n ) (1\le k\le n) (1kn)
  • 2 l r k:表示查询编号在 [ l , r ] [l,r] [l,r] 内的集合的并集中,第 k k k 大的数是多少。 ( 1 ≤ k ≤ 2 31 − 1 ) (1\le k\le 2^{31}-1) (1k2311)

注意可重集的并是不去除重复元素的,如 { 1 , 1 , 4 } ∪ { 5 , 1 , 4 } = { 1 , 1 , 4 , 5 , 1 , 4 } \{1,1,4\}\cup\{5,1,4\}=\{1,1,4,5,1,4\} {1,1,4}{5,1,4}={1,1,4,5,1,4}​。

题目链接

如果运用整体二分算法,我们相当于将问题转化为:

有一序列支持:

  1. 区间加 1。
  2. 区间求和。

用线段树可以轻松解决,也可以用树状数组+差分解决这一问题。下面给出用树状数组实现的全代码:

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
const int M=5e4+5;

inline ll read()
{
	ll x=0,f=1;static char ch;
	while(ch=getchar(),ch<48)if(ch==45)f=0;
	do x=(x<<1ll)+(x<<3ll)+(ch^48);
	while(ch=getchar(),ch>=48);
	return f?x:-x;
}

int n,m,Ans[M];
bool out[M];

struct cpp
{
	int op,l,r,id;ll c;
	void init(int i)
	{
		op=read(),l=read(),r=read();
		c=read(),id=i;
		if(op==2)out[i]=true;
	}
}q[M],ql[M],qr[M];

struct BIT
{
	ll c1[M],c2[M];
	void init()
	{
		memset(c1,0,sizeof(c1));
		memset(c2,0,sizeof(c2));
	}
	#define lowbit(x) x&(-x)
	void add(int i,ll x)
	{
		int p=i;
		while(i<=n)c1[i]+=x,c2[i]+=x*(p-1),i+=lowbit(i);
	}
	ll query(int i)
	{
		int p=i;ll res=0;
		while(i>0)res+=c1[i]*p-c2[i],i-=lowbit(i);
		return res;
	}
}T;
/*支持区间+1区间求和的树状数组*/
void solve(int l,int r,int L,int R)
{
	if(l==r)
	{
		for(int i=L;i<=R;i++)
			if(q[i].op==2)Ans[q[i].id]=l;
		 /*值为l答案统计现场*/
		return;
	}
	int mid=(l+r)>>1;
	int cnt1=0,cnt2=0;
	/*
	由于是求第k大,与上述代码需要相反
	即寻找左右区间的相反(ql和qr数组) 
	*/
	for(int i=L;i<=R;i++)
	{
		if(q[i].op==1)
		{
			if(mid<q[i].c)
			{
				T.add(q[i].l,1);
				T.add(q[i].r+1,-1);
				/*区间修改,含差分思想*/
				qr[++cnt2]=q[i];
			}
			else ql[++cnt1]=q[i];
		}
		else
		{
			ll num=T.query(q[i].r)-T.query(q[i].l-1);
			/*区间求和*/
			if(num>=q[i].c)qr[++cnt2]=q[i];
			else q[i].c-=num,ql[++cnt1]=q[i];
		}
	}
	/*注意到如果即是修改又在qr数组内
	必然是区间修改操作
	直接用add(-1)的方法撤销即可
	*/
	for(int i=1;i<=cnt2;i++)
		if(qr[i].op==1)
		{
			T.add(qr[i].l,-1);
			T.add(qr[i].r+1,1);
		}
	/*还是一样的复制操作*/
	for(int i=1;i<=cnt1;i++)
		q[i+L-1]=ql[i];
	for(int i=1;i<=cnt2;i++)
		q[i+L+cnt1-1]=qr[i];
	solve(l,mid,L,L+cnt1-1);
	solve(mid+1,r,L+cnt1,R);
}
int main()
{
	T.init();
	n=read(),m=read();
	for(int i=1;i<=m;i++)
		q[i].init(i);
	
	solve(1,n,1,m);
	/*
	值域:1-n
	操作:1-m 
	*/
	for(int i=1;i<=m;i++)
		if(out[i])printf("%d\n",Ans[i]);
	return 0;
}

但如果复杂度只能与操作个数相关,不能与序列长度线性相关呢?(即序列长度达到 1 0 9 10^9 109 级别,此时线段树或树状数组难以支持上述操作)

注意到这些修改操作满足”修改独立“这一性质。因此我们采用对时间分治的方法,将问题转化为:

给出若干区间,求该区间与之前所有区间的交集的长度之和。

直接排序后扫一遍即可解决,时间复杂度为 O ( n log ⁡ 3 n ) O(n\log^3 n) O(nlog3n),用归并排序可优化至 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n)

代码实现?全网都没有为什么我会写

例题2

矩阵乘法

给定 n × m n\times m n×m 的矩阵,查询某个子矩阵的第 k k k​ 大值。

题目链接

一维 → \rightarrow ​ 二维。仍然是一样的套路,只不过变为统计当前询问子矩阵中 ≤ m i d \le mid mid 数的个数。这个东西可以用二维树状数组去维护,实现起来也比较简单。

具体代码实现

例题3

[POI2011]MET-Meteors

Byteotian Interstellar Union 有 n n n​ 个成员国。现在它发现了一颗新的星球,这颗星球的轨道被分为 m m m 份(第 m m m 份和第 1 1 1 份相邻),第 i i i 份上有第 a i a_i ai 个国家的太空站。

这个星球经常会下陨石雨。BIU 已经预测了接下来 k k k 场陨石雨的情况。

BIU 的第 i i i 个成员国希望能够收集 p i p_i pi 单位的陨石样本。你的任务是判断对于每个国家,它需要在第几次陨石雨之后,才能收集足够的陨石。

题目链接

前面所有的题目几乎都是一个板子套来套去,接下来我们看看这道整体二分的经典例题。

容易发现答案具有单调性:设 t t t 为答案,那么 < t <t <t 的时刻都不满足条件, ≥ t \ge t t​ 的时刻都满足。每个国家都二分一次的时间复杂度显然是无法承受的,因此我们考虑整体二分。

我们将时间在 [ l , m i d ] [l,mid] [l,mid] 间的陨石雨全部降下,统计得到该国家获得的陨石数。如果陨石数 ≥ p i \ge p_i pi,则说明 t ∈ [ l , m i d ] t\in [l,mid] t[l,mid],否则 t ∈ [ m i d + 1 , r ] t\in [mid+1,r] t[mid+1,r]​。陨石数可以用树状数组维护,区间修改单点查询。由于是一个环,我们可以破环成链,但也可以增加一个 if(l>r)add(1,k) 的操作。这是等效的,但后者常数更小。

具体代码实现

值得注意的是,本题代码写出来一般常数较大,这里有几处优化:

  1. 用索引排序,即仅记录对应下标而不是整个复制结构体。由于结构体内含的参数过多,每个数字复制时都带有一定的常数,累加起来会较慢。而仅复制下标常数更小。
  2. 区间修改可以直接改为几次单点的修改操作。在更改为单点修改的前提下,不用树状数组维护这些统计,而用一个桶维护。去掉所有查询操作,将所有修改操作按照修改的端点排序,统计时直接根据索引找到对应的修改即可。不使用树状数组可以少个 log ⁡ \log log
  3. 读入输出挂。即 IO 优化。

在这里我直接贴别人代码了(

具体代码实现

推荐习题


参考文献

  • 许昊然《浅谈数据结构题几个非经典解法》(2013年信息学奥林匹克中国国家队候选队员论文)
  • OI Wiki - 整体二分
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值