刷题杂记之整体二分

二分答案是一个非常优秀的算法,对于一个答案具有单调性的性质,一般都可以用二分来快速解答

但是如果有多个询问呢?
如果每一个询问都是一样的,且具有单调性,而且题目不要求强制在线,就可以用整体二分来求解(其实如果能够用q次二分答案的估计就可以整体二分)

整体二分的具体思路
先将所有的问题离线下来,将问题和答案一起进行二分
将答案的范围找一个中间值mid1,然后再在问题里面一个个比较
如果发现答案比这个大,那么就把这个放到后面的位置
如果发现答案比这个小,那么就把这个放到前面的位置
这样最后这个区间就变成了一个以mid2为中心分开
mid2的左边是关于mid1可行的,右边是不可行的
然后分开递归
最后如果答案范围只有1的空间了,那么就可以记录了
需要注意的是,在整个递归里面,不能超过O(n)级别,不然…(不会算时间复杂度)
所以再整体二分里面,可以套用一些数据结构(线段树,树状数组)用来达到能减少复杂度的目的

题目传送门
  • 如果只有一个询问,那么很显然,可以利用二分答案来求解,只要记录小于mid的个数,就可以判断了
  • 但是有多个询问,考虑整体二分,但是如何高效的处理每一个问题
  • 非常简单的能够想到可以通过一个二维数组a[i][j]表示到(i,j)的大于mid的数,那么只要O(n^2)处理,O(R-L+1)处理询问就行了
  • 但是这样还是不够,万一有个每个问题都均匀分布呢
  • 看到处理询问的时间很短,但是预处理很长,所以可以通过结构体来储存每一个的下标和数字,然后按数字从小到大排序,每一次只要计算(l~mid)就行了
  • 为什么?考虑一下整体二分的性质,首先对于在mid2左边的一些数,我们目前计算的对它并没有什么影响,所以先往下递归,然后对于在mid2右边的数,这些计算是对它有影响的,所以只要用当前的个数k减去消除的个数就能够排除影响了,这样每一个数只会被统计一次
  • 时间复杂度O( ( ( ( N 2 N^2 N2 + Q ) +Q) +Q) l o g 3 N log^3N log3N )(我也不知道怎么算)
#include<bits/stdc++.h>
using namespace std;
const int N=6e4+10;
const int M=510;
struct cow{int x1,y1,x2,y2,k,id;}Q[N],_l[N],_r[N];
struct COW{int x,y,z;}a[M*M];
int read()
{
	int num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
int n,q,Ans[N],c[M][M];
#define lowbit(x) (x&-x)
int getsum(int x,int y)
{
	int sum=0;
	for(;x;x-=lowbit(x))
	  for(int i=y;i;i-=lowbit(i))
	    sum+=c[x][i];
	return sum;
}
void add(int x,int y,int z)
{
	for(;x<=n;x+=lowbit(x))
	  for(int i=y;i<=n;i+=lowbit(i))
	    c[x][i]+=z;
}
void solve(int L,int R,int l,int r)
{
	if(l==r){
		for(int i=L;i<=R;i++)
		  Ans[Q[i].id]=a[l].z;
		return ;
	}
	int mid=l+r>>1,l_=0,r_=0;
	for(int i=l;i<=mid;i++)add(a[i].x,a[i].y,1);
	for(int i=L;i<=R;i++)
	{
		int ans=getsum(Q[i].x2,Q[i].y2)-getsum(Q[i].x2,Q[i].y1-1)
		     -getsum(Q[i].x1-1,Q[i].y2)+getsum(Q[i].x1-1,Q[i].y1-1);
		if(ans>=Q[i].k)_l[++l_]=Q[i];
		  else _r[++r_]=Q[i],_r[r_].k-=ans;
	}
	for(int i=l;i<=mid;i++)add(a[i].x,a[i].y,-1);
	for(int i=1;i<=l_;i++)Q[i+L-1]=_l[i];
	for(int i=1;i<=r_;i++)Q[L+l_+i-1]=_r[i];
	solve(L,L+l_-1,l,mid);solve(R-r_+1,R,mid+1,r);
}
bool cmp(COW a,COW b){return a.z<b.z;}
int main()
{
	n=read();q=read();
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=n;j++)
	    a[(i-1)*n+j]=(COW){i,j,read()};
	sort(a+1,a+n*n+1,cmp);
	for(int i=1;i<=q;i++)
	{
		Q[i].x1=read();Q[i].y1=read();
		Q[i].x2=read();Q[i].y2=read();
		Q[i].k=read();Q[i].id=i;
	}
	solve(1,q,1,n*n);
	for(int i=1;i<=q;i++)
	  printf("%d\n",Ans[i]);
	return 0;
}
题目传送门
  • 这题一眼看下去,咋这么像线段树模板呢
  • 首先思考只有一种的情况,那么很显然,二分k的位置,然后再用树状数组在里面维护,做到O( n l o g 2 n nlog^2n nlog2n)
  • 但是有多个询问,因为这个也是具有单调性的,考虑整体二分
  • 和上题一模一样,只要用树状数组维护一下区间和,然后左边的不变,右边的减一下,递归下去就行了
  • 简直一模一样
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10;
#define ll long long
#define lowbit(x) (x&-x)
int n,m,k,Ans[N],now;ll c[N<<1];vector<int>E[N];
struct cow{int id;ll x;}q[N],_l[N],_r[N];
struct COW{int x,y;ll z;}e[N];
void add(int x,ll z){
	for(;x<=2*m;x+=lowbit(x))
	  c[x]+=z;
}
ll getsum(int x){
	ll sum=0;
	for(;x;x-=lowbit(x))
	  sum+=c[x];
	return sum;
}
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;
		return ;
	}
	int mid=l+r>>1,l_=0,r_=0;
	for(int i=l;i<=mid;i++)add(e[i].x,e[i].z),add(e[i].y+1,-e[i].z);
	for(int i=L;i<=R;i++)
	{
		ll sum=0;
		for(int j=0;j<E[q[i].id].size()&&sum<=q[i].x;j++)
		  sum+=getsum(E[q[i].id][j])+getsum(E[q[i].id][j]+m);
		if(sum>=q[i].x)_l[++l_]=q[i];
	      else _r[++r_]=q[i],_r[r_].x-=sum;
    }
	for(int i=l;i<=mid;i++)add(e[i].x,-e[i].z),add(e[i].y+1,e[i].z);
	for(int i=1;i<=l_;i++)q[L+i-1]=_l[i];
	for(int i=1;i<=r_;i++)q[L+l_+i-1]=_r[i];
	solve(L,L+l_-1,l,mid);solve(R-r_+1,R,mid+1,r);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,o;i<=m;i++)scanf("%d",&o),E[o].push_back(i);
	for(int i=1;i<=n;i++)scanf("%lld",&q[i].x),q[i].id=i;
	scanf("%d",&k);
	for(int i=1;i<=k;i++)scanf("%d%d%lld",&e[i].x,&e[i].y,&e[i].z);
	for(int i=1;i<=k;i++)if(e[i].y<e[i].x)e[i].y+=m;
	solve(1,n,1,k+1);
	for(int i=1;i<=n;i++)
	  if(Ans[i]==k+1)printf("NIE\n");
	    else printf("%d\n",Ans[i]);
	return 0;
}
题目传送门
  • 这题有很多很多的做法,比如树套树

-------------------------------------------------------------------------------------------------

  • 那么考虑整体二分
  • 有很多的问题
  • 前面两道题目并没有修改的操作,而在这一题有这不是废话吗,还有的就是顺序的问题
  • 那么考虑对于这两个询问怎么处理,首先同理一样,对于询问的操作,跟上面一样判断就行(就是加一个线段树
  • 对于添加的操作,假设添加一个数C,当前的二分l,r的中间值是mid
  • 那么假如C<=mid,那么它对于右边的区间是没有影响的(显然),那么只要加入左区间就行了,相反则加入右区间
  • 那么就这么简洁愉快的解决了
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10;
#define ll long long
ll read()
{
	ll num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
struct Tree
{
	struct cow{
		int l,r;ll sum,lazy;
		#define l(p) tree[p].l
		#define r(p) tree[p].r
		#define sum(p) tree[p].sum
		#define lazy(p) tree[p].lazy
	}tree[N<<2];
	void build(int p,int l,int r){
		l(p)=l;r(p)=r;
		if(l==r)return ;
		int mid=l+r>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
	}
	void lazytime(int p){
		sum(p<<1)+=(r(p<<1)-l(p<<1)+1)*lazy(p);
		sum(p<<1|1)+=(r(p<<1|1)-l(p<<1|1)+1)*lazy(p);
		lazy(p<<1)+=lazy(p);lazy(p<<1|1)+=lazy(p);lazy(p)=0;
	}
	ll ask(int p,int l,int r){
		if(l(p)>=l&&r(p)<=r)return sum(p);
		lazytime(p);ll ans=0;
		int mid=l(p)+r(p)>>1;
		if(l<=mid)ans+=ask(p<<1,l,r);
		if(r>mid)ans+=ask(p<<1|1,l,r);
		return ans;
	}
	void change(int p,int l,int r,int c){
		if(l(p)>=l&&r(p)<=r){
			sum(p)+=(r(p)-l(p)+1)*c;
			lazy(p)+=c;return ;
		}
		lazytime(p);int mid=l(p)+r(p)>>1;
	    if(l<=mid)change(p<<1,l,r,c);
	    if(r>mid)change(p<<1|1,l,r,c);
	    sum(p)=sum(p<<1)+sum(p<<1|1);
	}
}Tree;
struct cow{int opt,l,r,id;ll c;}q[N],_l[N],_r[N];
int n,m,tot;ll Ans[N];
void solve(int l,int r,int L,int R)
{
	if(l==r){
		for(int i=L;i<=R;i++)
		  if(q[i].opt==2)Ans[q[i].id]=l;
		return ;
	}
	int mid=l+r>>1,l_=0,r_=0;bool LL=0,RR=0;
	for(int i=L;i<=R;i++)
	{
	  if(q[i].opt==1){
	  	if(q[i].c<=mid)_l[++l_]=q[i];
	  	  else Tree.change(1,q[i].l,q[i].r,1),_r[++r_]=q[i];
	  }
	  else{
	  	ll sum=Tree.ask(1,q[i].l,q[i].r);
	  	if(sum<q[i].c)q[i].c-=sum,_l[++l_]=q[i],LL=1;
	  	  else _r[++r_]=q[i],RR=1;
	  }
    }
	for(int i=1;i<=l_;i++)q[L+i-1]=_l[i];
	for(int i=1;i<=r_;i++){
		q[L+l_+i-1]=_r[i];
		if(_r[i].opt==2)continue;
		Tree.change(1,_r[i].l,_r[i].r,-1);
	}
	if(LL)solve(l,mid,L,L+l_-1);
	if(RR)solve(mid+1,r,L+l_,R);
}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=m;i++){
		q[i].opt=read();
		q[i].l=read();q[i].r=read();
		q[i].c=read();q[i].id=i;
	}Tree.build(1,1,n);
	solve(-n,n,1,m);
	for(int i=1;i<=m;i++)
	  if(Ans[i])printf("%lld\n",Ans[i]);
	return 0;
}
题目传送门
  • 最后亿题了
  • 首先想想只有一个的时候怎么二分,显然二分美味度,再对符合美味度的按照价格从小到大进行排序,然后从小到大选。时间复杂度:O( n l o g 2 n nlog^2n nlog2n)(主要的瓶颈在于需要排序)
  • 那么考虑整体二分
  • 同样是通过mid判断左右,但是主要的问题是如何快速的判断是否可行
  • 我们可以通过以价格为下标维护树状数组
  • 让后我们只需要将回溯的位置变一下(放在一个l~mid的递归之后)
  • 就能轻松解决这个问题了
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define lowbit(x) (x&-x)
ll read()
{
	ll num=0;bool flag=1;
	char c=getchar();
	for(;c<'0'||c>'9';c=getchar())
	  if(c=='-')flag=0;
	for(;c>='0'&&c<='9';c=getchar())
	  num=(num<<1)+(num<<3)+c-'0';
	return flag?num:-num;
}
const int N=1e5+10;
struct cow{int d,p,l;}x[N];
struct COW{ll g,l;int id;}q[N],_l[N],_r[N];
int n,m,Ans[N];ll cl[N],cp[N];
bool cmp(cow a,cow b){return a.d<b.d;}
void add(int p,int l,int z){
	for(int i=p;i<=N-10;i+=lowbit(i))
	  cl[i]+=z*l,cp[i]+=1ll*z*l*p;
}
ll getsum_l(int x){
	ll sum=0;
	for(;x;x-=lowbit(x))
	  sum+=cl[x];
	return sum;
}
ll getsum_p(int x){
	ll sum=0;
	for(;x;x-=lowbit(x))
	  sum+=cp[x];
	return sum;
}
int dbl(int l,int r,ll val)
{
	while(l<r){
		int mid=l+r>>1;
		if(getsum_l(mid)>=val)r=mid;
		  else l=mid+1;
	}
	return l;
}
void solve(int L,int R,int l,int r)
{
	if(l==r){
		for(int i=L;i<=R;i++)
		  Ans[q[i].id]=x[l].d;
		return ;
	}
	int mid=l+r+1>>1,l_=0,r_=0;
	//分成l~mid-1和mid~r两段区间
	//因为并不知道如果刚好等于的话是在左区间还是右区间
	//(有可能后面还有更优的) 
	for(int i=mid;i<=r;i++)
	  add(x[i].p,x[i].l,1);
	for(int i=L;i<=R;i++)
	{
		int cnt=dbl(0,N-10,q[i].l);
		ll ans_l=getsum_l(cnt),ans_p=getsum_p(cnt);
		if(ans_l>=q[i].l&&q[i].g>=ans_p-(ans_l-q[i].l)*cnt)
		  _r[++r_]=q[i];else _l[++l_]=q[i];
	}
	for(int i=1;i<=l_;i++)q[i+L-1]=_l[i];
	for(int i=1;i<=r_;i++)q[L+l_+i-1]=_r[i]; 
	solve(L,L+l_-1,l,mid-1);
	for(int i=mid;i<=r;i++)
	  add(x[i].p,x[i].l,-1);
	solve(R-r_+1,R,mid,r);
}
int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
	{
		x[i].d=read();
		x[i].p=read();
		x[i].l=read();
	}x[0]=(cow){-1,1,1};
	sort(x,x+n+1,cmp);
	for(int i=1;i<=m;i++)
	{
		q[i].g=read();
		q[i].l=read();
		q[i].id=i;
	}
	solve(1,m,0,n);
	for(int i=1;i<=m;i++)
	  printf("%d\n",Ans[i]);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值