整体二分题解

整体二分算法题解

整体二分(Parallel Binary Search)是一种用于解决多组查询问题的有效算法,特别适用于每组查询的答案都可以通过二分查找来确定的情况。

算法思想

你说得对,整体二分的基本思想就是将所有查询一起处理,通过二分答案区间,同时对多个查询进行判定和分类。这种方法的优势在于可以共享计算过程,减少重复计算。

那么,二分该如何分呢?

对于数值,简单地把所有的数值按值域割开;对于查询,按照查找结果是否应在左区间,如果是,直接放在左区间;否则就放在右区间。

但对于上面放在右区间的操作,我们需要消除左区间对该查询操作的贡献,具体的可以画个图理解一下(老师的图不敢搬qwq)。

例题

1. 区间第 kkk

问题模型:在一个数列中查找某区间的第 kkk 小。

模板+树状数组

为了方便,这里把数组的每一个数都看成一次修改操作。

可以发现,在分割查看所属区间时,我们直接暴力查找会悲催地 TLE,于是我们就想到了可以套一个数据结构——树状数组中,单点加,区间查询即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int ans[N],n,m,cnt;
map<int,int> hash_,rh;
struct node{
	int val,pos;
}a[N];
struct qurr{
	int L,R,id,k,type;
}b[N];
int s;
qurr q1[N],q2[N];
int cnt1,cnt2;
int tr[N];
int tt;
bool cmp(node x,node y)
{
	return x.val<y.val;
}
int qur(int p)
{
	int ans=0;
	while(p)
	{
		ans+=tr[p];
		p-=p&(-p);
	}
	return ans;
}
void add(int p,int x)
{
	while(p<=n)
	{
		tr[p]+=x;
		p+=p&(-p);
	}
}
void solve(int l,int r,int ql,int qr)
{
	if(ql>qr) return ;
	if(l==r)
	{
		for(int i=ql; i<=qr; i++)
		{
			if(b[i].type==2) ans[b[i].id]=rh[l];
		}
		return ;
	}
	int mid=(l+r)/2,cnt1=0,cnt2=0;
	for(int i=ql; i<=qr; i++)
	{
		if(b[i].type==1)
		{
			if(b[i].L<=mid)
			{
				add(b[i].id,1);
				q1[++cnt1]=b[i];
			}
			else
			{
				q2[++cnt2]=b[i];
			}
		}
		else
		{
			int x=b[i].L;
			int y=b[i].R;
			int tem=qur(y)-qur(x-1);
			if(b[i].k>tem) b[i].k-=tem,q2[++cnt2]=b[i];
			else q1[++cnt1]=b[i];
		}
	}
	for(int i=1; i<=cnt1; i++)
	{
		if(q1[i].type==1) add(q1[i].id,-1);
	}
	for(int i=1; i<=cnt1; i++)
	{
		b[i+ql-1]=q1[i];
	}
	for(int i=1; i<=cnt2; i++)
	{
		b[i+cnt1+ql-1]=q2[i];
	}
	solve(l,mid,ql,cnt1+ql-1);
	solve(mid+1,r,cnt1+ql,qr);
}
int main(){
	cin>>n>>m;
	for(int i=1; i<=n; i++)
	{
		cin>>a[i].val;
		a[i].pos=i;
	}
	sort(a+1,a+1+n,cmp);
	for(int i=1; i<=n; i++)
	{
		int tem=a[i].val;
		if(!hash_[tem])
		{
			hash_[tem]=++cnt;
			rh[cnt]=tem;
		}
		b[++s]={hash_[tem],-1,a[i].pos,-1,1};
	}
	for(int i=1; i<=m; i++)
	{
		int x,y,k;
		cin>>x>>y>>k;
		b[++s]={x,y,i,k,2};
	}
	solve(0,cnt+1,1,s);
	for(int i=1; i<=m; i++)
	{
		cout<<ans[i]<<"\n";
	}
	
	return 0;
}

2. 带修改的区间第k小

问题模型:修改某数值+查询局部第 kkk 小值。

大佬们可能会使用主席树,但这题我还是主要介绍整体二分的做法趴~

同样的,我们也把数组的数堪称修改操作,把修改和查询合并,用一个标识符 typetypetype 记录操作类型。

为了方便,我们可以把修改转化为加减,即 ai=ya_i=yai=y 转化为 ai−=ai,ai+=ya_i-=a_i,a_i+=yai=ai,ai+=y

主要的实现就是这些。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int ans[N],n,m,cnt;
map<int,int> hash_,rh;
int a[N];
struct qurr{
	int L,R,id,k,dx,type;
}q[N];
qurr q1[N],q2[N];
int cnt1,cnt2;
int tr[N];
int tt;
int qur(int p)
{
	int ans=0;
	while(p)
	{
		ans+=tr[p];
		p-=p&(-p);
	}
	return ans;
}
void add(int p,int x)
{
	while(p<=n)
	{
		tr[p]+=x;
		p+=p&(-p);
	}
}
void solve(int l,int r,int ql,int qr)
{
	if(ql>qr) return ;
	if(l==r)
	{
		for(int i=ql; i<=qr; i++)
		{
			if(q[i].type==2) ans[q[i].id]=l;
		}
		return ;
	}
	int mid=(l+r)/2,cnt1=0,cnt2=0;
	for(int i=ql; i<=qr; i++)
	{
		if(q[i].type==1)
		{
			if(q[i].R<=mid)
			{
				add(q[i].id,q[i].dx);
				q1[++cnt1]=q[i];
			}
			else
			{
				q2[++cnt2]=q[i];
			}
		}
		else
		{
			int x=q[i].L;
			int y=q[i].R;
			int tem=qur(y)-qur(x-1);
			if(q[i].k>tem) q[i].k-=tem,q2[++cnt2]=q[i];
			else q1[++cnt1]=q[i];
		}
	}
	for(int i=1; i<=cnt1; i++)
	{
		if(q1[i].type==1) add(q1[i].id,-q1[i].dx);
	}
	for(int i=1; i<=cnt1; i++)
	{
		q[i+ql-1]=q1[i];
	}
	for(int i=1; i<=cnt2; i++)
	{
		q[i+cnt1+ql-1]=q2[i];
	}
	solve(l,mid,ql,cnt1+ql-1);
	solve(mid+1,r,cnt1+ql,qr);
}
int sum;
int main(){
	cin>>n>>m;
	for(int i=1; i<=n; i++)
	{
		cin>>a[i];
		q[++sum]={i,a[i],i,-1,1,1};
	}
	for(int i=1; i<=m; i++)
	{
		string op;
		cin>>op;
		int x,y,k;
		if(op=="Q")
		{
			cin>>x>>y>>k;
			q[++sum]={x,y,i,k,-1,2};
		}
		else
		{
			cin>>x>>y;
			q[++sum]={x,a[x],x,-1,-1,1};
			q[++sum]={x,y,x,-1,1,1};
			a[x]=y;
		}
	}
	for(int i=1; i<=m; i++)
	{
		ans[i]=-1;
	}
	solve(0,1e9,1,sum);
	for(int i=1; i<=m; i++)
	{
		if(ans[i]!=-1)
		{
			cout<<ans[i]<<"\n";
		}
	}
	
	return 0;
}

3. 整体二分与单调序列

问题模型:给你一个序列,每次可以把一个数 +1 或 -1,要求把数列变成单调不降的,并且修改后的数只能出现修改前的数。

啊啊啊懒得写了。

算了,我还是写一写趴。

这里有一个性质:在满足次数最小化的前提下,一定存在一个方案是的最后序列中的每个数都是序列修改前存在的,这个可以使用数学归纳法证明一下。

考虑整体二分:

数据删除

手动狗头

算法分析

  1. 时间复杂度:整体二分的时间复杂度通常为O((N+Q)logNlogC),其中N是数据规模,Q是查询次数,C是值域范围。

  2. 空间复杂度:主要取决于递归深度和存储查询的空间,通常为O(N+Q)。

  3. 优势

    • 可以批量处理多个查询
    • 共享计算过程,减少重复计算
    • 代码结构清晰,易于理解和实现

注意事项

  1. 离散化处理:对于大值域问题,通常需要先进行离散化以提高效率。
  2. 树状数组/线段树:在整体二分中通常需要配合使用树状数组或线段树来统计区间信息。
  3. 边界条件:需要特别注意二分过程中的边界条件处理。
  4. 内存管理:对于大规模数据,需要注意内存使用情况。

Tips:本文是作者手搓,有口误请指出qwq

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值