Gushing over Programming Girls|

BigSmall_En

园龄:3年2个月粉丝:3关注:5

2024-12-15 22:28阅读: 10评论: 0推荐: 0

整体二分review小记

整体二分,顾名思义,就是高级一点的二分。二分需要满足满足什么性质,单调性!

比如我们看整体二分可以解决的经典问题:查询 \([l,r]\) 区间内第 \(k\) 小的数(严谨起见先假设没有重复的数)。现在我们假设这个询问的答案为 \(x\),如果这个答案是对的,那么 \([l,r]\) 区间内小于等于 \(x\) 的数恰好为 \(k\) 个;如果 \([l,r]\) 区间内小于等于 \(x\) 的数小于 \(k\) 个,那么这个答案 \(x\) 就偏小了;如果区间 \([l,r]\) 区间内小于等于 \(x\) 超过 \(k\) 个,那么这个答案 \(x\) 就偏大了。

那么我们每次二分答案 \(x\),将小于等于 \(x\) 的数加入数据结构中,对于每个询问,如果 \([l,r]\) 范围内数的个数小于 \(k\),那么说明这个数的答案大于 \(x\),将他归类到一类询问(叫做右询问吧)中,否则将他归类到另一类询问中。

那么在右询问中,我们扩大二分的答案为 \(y>x\),将在 \((x,y]\) 范围内的数加入数据结构中,继续执行上面的操作。

在左询问中,我们需要缩小二分答案的范围为 \(z<x\),将在 \((z,x]\) 范围内的数剔除出数据结构,继续执行上面的操作。(实际写的时候为了方便我们是将二分答案 \(x\) 时所有加入数据结构的数都剔除,再在左询问的时候把小于等于 \(z\) 的数加入数据结构中,复杂度是一样的)

递归进行这样的操作,层数为 \(\log n\),也就是每个询问最多判断 \(\log n\) 次,每个“修改”(我们把原序列看成一开始的修改)最多在数据结构中操作 \(\log n\) 次,总复杂度为 \(O(k(n+q)\log n)\),其中 \(k\) 与所使用的数据结构有关。

LG3834 基础模板

给定 \(n\) 个整数构成的序列 \(a\),将对于指定的闭区间 \([l, r]\) 查询其区间内的第 \(k\) 小值。

一种不那么严谨的写法。

void solve(int L,int R,int l,int r){
	if(L>R)return;
	int mid=(l+r)>>1,cl=0,cr=0;
	for(int i=L;i<=R;++i){
		if(op[i].opt==1){
			if(op[i].val<=mid){
				c.update(op[i].l,1);//树状数组
				lop[++cl]=op[i];
			}else rop[++cr]=op[i];
		}else{
			int tmp=c.querysum(op[i].l,op[i].r);
			if(op[i].val<=tmp){
				op[i].ans=mid;//在这里赋值答案
				lop[++cl]=op[i];
			}
			else rop[++cr]=op[i];
		}
	}
	for(int i=1;i<=cl;++i)op[L+i-1]=lop[i];
	for(int i=1;i<=cr;++i)op[L+cl+i-1]=rop[i];
	if(l<r)solve(L+cl,R,mid+1,r);//这种写法在上面没有特判 l==r 的情况,需要在此特判
	for(int i=L;i<L+cl;++i)if(op[i].opt==1)c.update(op[i].l,-1);
	if(l<r)solve(L,L+cl-1,l,mid);
}

推荐的板子

对于有些问题,可能一个询问没有合法的答案,因此我们可以考虑下面的写法。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<int,int>ttfa;

const int N=200005;
int n,a[N],m,lis[N],tot;
struct option{
	int opt,l,r,val,id,ans;
}op[N*2],lop[N*2],rop[N*2];int q;

struct BIT{
	int n,c[N];
	inline int lowbit(int x){return x&-x;}
	inline void update(int i,int v){
		for(;i<=n;i+=lowbit(i))c[i]+=v;
	}
	inline int getsum(int i,int x=0){for(;i;i-=lowbit(i))x+=c[i];return x;}
	inline int querysum(int l,int r){return getsum(r)-getsum(l-1);}
}c;

void solve(int L,int R,int l,int r){
	if(L>R)return;
	if(l==r){
		for(int i=L;i<=R;++i){
			if(op[i].opt==1)c.update(op[i].l,1);
			else{
				int tmp=c.querysum(op[i].l,op[i].r);
				if(op[i].val<=tmp)op[i].ans=l;//判断答案是否合法
				else op[i].ans=0;
			}
		}
		for(int i=L;i<=R;++i)
			if(op[i].opt==1)c.update(op[i].l,-1);
		return;
	}
	int mid=(l+r)>>1,cl=0,cr=0;
	for(int i=L;i<=R;++i){
		if(op[i].opt==1){
			if(op[i].val<=mid){
				c.update(op[i].l,1);
				lop[++cl]=op[i];
			}else rop[++cr]=op[i];
		}else{
			int tmp=c.querysum(op[i].l,op[i].r);
			if(op[i].val<=tmp){
				op[i].ans=mid;//在这个区间内显然是有合法答案的
				lop[++cl]=op[i];
			}
			else rop[++cr]=op[i];
		}
	}
	for(int i=1;i<=cl;++i)op[L+i-1]=lop[i];
	for(int i=1;i<=cr;++i)op[L+cl+i-1]=rop[i];
	solve(L+cl,R,mid+1,r);
	for(int i=L;i<L+cl;++i)if(op[i].opt==1)c.update(op[i].l,-1);
	solve(L,L+cl-1,l,mid);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
		lis[++tot]=a[i];
	}
	sort(lis+1,lis+1+tot);
	tot=unique(lis+1,lis+1+tot)-lis-1;
	for(int i=1;i<=n;++i){
		a[i]=lower_bound(lis+1,lis+1+tot,a[i])-lis;
		op[++q]=(option){1,i,i,a[i],m+i,0};
	}
	for(int i=1;i<=m;++i){
		int l,r,k;scanf("%d%d%d",&l,&r,&k);
		op[++q]=(option){2,l,r,k,i,0};
	}
	c.n=n;solve(1,q,1,tot);
	sort(op+1,op+1+q,[](const option&x,const option&y){return x.id<y.id;});
	for(int i=1;i<=m;++i)
		printf("%d\n",lis[op[i].ans]);
	return 0;
}

不建议使用下面的写法(因为有些需要题不像区间第 \(k\) 小一样简单,可能真的需要用到整个序列中小于等于二分答案 \(x\) 的数)

void solve(int L,int R,int l,int r){
	if(L>R)return;
	if(l==r){
		for(int i=L;i<=R;++i)
			if(op[i].opt==2)op[i].ans=l;
		return;
	}
	int mid=(l+r)>>1,cl=0,cr=0;
	c.f=1;
	for(int i=L;i<=R;++i){
		if(op[i].opt==1){
			if(op[i].val<=mid){
				c.update(op[i].l,1);
				lop[++cl]=op[i];
			}else rop[++cr]=op[i];
		}else{
			int tmp=c.querysum(op[i].l,op[i].r);
			if(op[i].val<=tmp)lop[++cl]=op[i];
			else{
				op[i].val-=tmp;//答案满足可加性,也就是说右询问不需要考虑左询问的数
				rop[++cr]=op[i];
			}
		}
	}
	for(int i=1;i<=cl;++i){//直接在这里清空了
        op[L+i-1]=lop[i];
    	if(lop[i].opt==1)update(lop[i].l,-1);
    }
	for(int i=1;i<=cr;++i)op[L+cl+i-1]=rop[i];
	solve(L,L+cl-1,l,mid);
	solve(L+cl,R,mid+1,r);
}

在上面这个问题中,虽然我们的“修改”都在查询前面,我们用同一种结构体存了两种类型的操作,这是为了方便询问中间的修改操作。

LG3332 待修问题

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

  • 1 l r c:表示将 \(c\) 加入到编号在 \([l,r]\) 内的集合中
  • 2 l r c:表示查询编号在 \([l,r]\) 内的集合的并集中,第 \(c\) 大的数是多少。

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

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
const int N=100005;
struct node{int opt,l,r,id;ll val;}a[N],al[N],ar[N];
int n,m,ans[N];
struct segtree{int left,right;ll sum,tag,clr;}t[N<<2];
#define ls p<<1//线段树维护区间加区间求和
#define rs p<<1|1
inline void pushdown(int p){
	if(t[p].clr){
		t[ls].tag=t[rs].tag=t[ls].sum=t[rs].sum=0;
		t[ls].clr=t[rs].clr=1;t[p].clr=0;
	}
	if(t[p].tag){
		t[ls].sum+=(t[ls].right-t[ls].left+1)*t[p].tag;
		t[rs].sum+=(t[rs].right-t[rs].left+1)*t[p].tag;
		t[ls].tag+=t[p].tag;t[rs].tag+=t[p].tag;
		t[p].tag=0;
	}return;
}
inline void pushup(int p){t[p].sum=t[ls].sum+t[rs].sum;}
void buildtree(int p,int l,int r){
	t[p].left=l,t[p].right=r;
	if(l==r){t[p].sum=0;return;}
	int mid=(l+r)>>1;
	buildtree(ls,l,mid);buildtree(rs,mid+1,r);
	pushup(p);
}
void update(int p,int l,int r,ll v){
	if(l<=t[p].left&&t[p].right<=r){
		t[p].sum+=(t[p].right-t[p].left+1)*v;
		t[p].tag+=v;return;
	}pushdown(p);
	if(l<=t[ls].right)update(ls,l,r,v);
	if(r>=t[rs].left)update(rs,l,r,v);
	pushup(p);
}
ll getsum(int p,int l,int r){
	if(l<=t[p].left&&t[p].right<=r)
		return t[p].sum;
	pushdown(p);ll tmp=0;
	if(l<=t[ls].right)tmp+=getsum(ls,l,r);
	if(r>=t[rs].left)tmp+=getsum(rs,l,r);
	return tmp;
}
void solve(int l,int r,int L,int R){
	if(l>r)return;
	if(L==R){
		for(int i=l;i<=r;++i)
			if(a[i].opt==2)ans[a[i].id]=L;
		return;
	}int mid=(L+R)>>1,p1=0,p2=0;
	t[1].clr=1;t[1].sum=t[1].tag=0;
	for(int i=l;i<=r;++i){
		if(a[i].opt==1){
			if(a[i].val>mid){
				update(1,a[i].l,a[i].r,1);
				ar[++p2]=a[i];
			}else al[++p1]=a[i];
		}else{
			ll tmp=getsum(1,a[i].l,a[i].r);
			if(a[i].val<=tmp)ar[++p2]=a[i];
			else{
				a[i].val-=tmp;
				al[++p1]=a[i];
			}
		}
	}
	for(int i=1;i<=p1;++i)a[l+i-1]=al[i];
	for(int i=1;i<=p2;++i)a[l+p1+i-1]=ar[i];
	solve(l,l+p1-1,L,mid);
	solve(l+p1,r,mid+1,R);
}
int main(){
	scanf("%d%d",&n,&m);
	buildtree(1,1,n);
	for(int i=1;i<=m;++i){
		scanf("%d%d%d%lld",&a[i].opt,&a[i].l,&a[i].r,&a[i].val);
		if(a[i].opt==2)a[i].id=++ans[0];
	}solve(1,m,-n,n);
	for(int i=1;i<=ans[0];++i)
		printf("%d\n",ans[i]);
	return 0;
}

LG4602 较为复杂的询问

商店里有 \(n\) 种果汁,编号为 \(0,1,\cdots,n-1\)\(i\) 号果汁的美味度是 \(d_i\),每升价格为 \(p_i\)。小 R 在制作混合果汁时,还有一些特殊的规定,即在一瓶混合果汁中,\(i\) 号果汁最多只能添加 \(l_i\) 升。

现在有 \(m\) 个小朋友过来找小 R 要混合果汁喝,他们都希望小 R 用商店里的果汁制作成一瓶混合果汁。其中,第 \(j\) 个小朋友希望他得到的混合果汁总价格不大于 \(g_j\),体积不小于 \(L_j\)。在上述这些限制条件下,小朋友们还希望混合果汁的美味度尽可能地高,一瓶混合果汁的美味度等于所有参与混合的果汁的美味度的最小值。请你计算每个小朋友能喝到的最美味的混合果汁的美味度。

这个询问显然也是满足单调性的,答案美味度越低,他能喝的果汁越多。所以实际上我们只要搞清楚子问题:只考虑美味度大于二分答案的果汁,是否可以满足小朋友的要求。

这个问题其实就是贪心,我们尽可能取价格少的果汁,将价格少的果汁能取满取满,用一个线段树优化这个问题。为下标 \(p_i\),维护能够取的体积和取满的价格。在线段树上二分即可。具体实现见代码吧,个人认为还是比较清晰的,虽然写得很长。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<int,int>ttfa;

const int N=200005;
int n,m,q,lis[N],tot;ll mxp;
struct node{
	int opt,id,val;
	ll pri,siz;
}op[N],lop[N],rop[N];

ll siz[N<<2],sum[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
inline void pushup(int p){
	siz[p]=siz[ls]+siz[rs];
	sum[p]=sum[ls]+sum[rs];
}
void build(int p,int l,int r){
	if(l==r){siz[p]=sum[p]=0;return;}
	build(ls,l,mid);build(rs,mid+1,r);
	pushup(p);
}
void update(int p,int l,int r,int L,ll x,ll y){
	//printf("update %d %d %d %d %lld %lld\n",p,l,r,L,x,y);
	if(l==r){siz[p]+=x,sum[p]+=y;return;}
	if(L<=mid)update(ls,l,mid,L,x,y);
	else update(rs,mid+1,r,L,x,y);
	pushup(p);
}

int findsum(int p,int l,int r,ll x){
	if(l==r){return l;}
	if(x<=sum[ls])return findsum(ls,l,mid,x);
	return findsum(rs,mid+1,r,x-sum[ls]);
}

ll getsum(int p,int l,int r,int L,int R,ll *s){
	if(L>R)return 0;
	if(L<=l&&r<=R)return s[p];
	ll tmp=0;
	if(L<=mid)tmp+=getsum(ls,l,mid,L,R,s);
	if(R>mid)tmp+=getsum(rs,mid+1,r,L,R,s);
	return tmp;
}

#undef ls
#undef rs
#undef mid

inline bool check(ll vol,ll lim){
	if(sum[1]<=lim)return siz[1]>=vol; 
	int loc=findsum(1,1,mxp,lim);
	ll pre_sum=getsum(1,1,mxp,1,loc-1,sum);
	ll pre_siz=getsum(1,1,mxp,1,loc-1,siz),n_siz=getsum(1,1,mxp,loc,loc,siz);
	return pre_siz+min((lim-pre_sum)/loc,n_siz)>=vol;
}

void solve(int L,int R,int l,int r){
	if(L>R)return;
	if(l==r){
		for(int i=L;i<=R;++i){
			if(op[i].opt==1)update(1,1,mxp,op[i].pri,op[i].siz,op[i].pri*op[i].siz);
			else if(check(op[i].siz,op[i].pri))op[i].val=l;
			else op[i].val=0;
		}
		for(int i=L;i<=R;++i)
			if(op[i].opt==1)update(1,1,mxp,op[i].pri,-op[i].siz,-op[i].pri*op[i].siz);
		return;
	}
	int mid=(l+r)>>1,cl=0,cr=0;
	for(int i=L;i<=R;++i){
		if(op[i].opt==1){
			if(op[i].val>mid){
				update(1,1,mxp,op[i].pri,op[i].siz,op[i].pri*op[i].siz);
				rop[++cr]=op[i];
			}else{
				lop[++cl]=op[i];
			} 
		}else{
			if(check(op[i].siz,op[i].pri)){
				op[i].val=mid+1;
				rop[++cr]=op[i];
			}
			else lop[++cl]=op[i];
		}
	}
	for(int i=1;i<=cl;++i)op[L+i-1]=lop[i];
	for(int i=1;i<=cr;++i)op[L+cl+i-1]=rop[i];
	solve(L,L+cl-1,l,mid);
	for(int i=L+cl;i<=R;++i)
		if(op[i].opt==1)update(1,1,mxp,op[i].pri,-op[i].siz,-op[i].pri*op[i].siz);
	solve(L+cl,R,mid+1,r);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		op[++q].opt=1;
		scanf("%d%lld%lld",&op[q].val,&op[q].pri,&op[q].siz);
		op[q].id=m+i;
		mxp=max(mxp,op[q].pri);
		lis[++tot]=op[q].val;
	}
	build(1,1,mxp);
	sort(lis+1,lis+1+tot);
	tot=unique(lis+1,lis+1+tot)-lis-1;
	for(int i=1;i<=m;++i){
		op[++q].opt=2;
		scanf("%lld%lld",&op[q].pri,&op[q].siz);
		op[q].id=i;
	}
	for(int i=1;i<=n;++i)op[i].val=lower_bound(lis+1,lis+1+tot,op[i].val)-lis;
	solve(1,q,1,tot);
	sort(op+1,op+1+q,[](node &x,node &y){return x.id<y.id;});
	lis[0]=-1;
	for(int i=1;i<=m;++i)
		printf("%d\n",lis[op[i].val]);
	return 0;
}

后续问题

其实这种题如果想到整体二分的话,整体二分就不是难点了,利用数据结构求解问题才是重点,因此有些整体二分套板子的题我就不写了。整体二分不仅可以用到这种序列上的问题,还能用到树上或者图上,因此思路放灵活。

后记

为什么从整体二分开始写这种专题呢?大概是因为三年前 NOIP 爆零之后好前几个学的就是整体二分。

本文作者:BigSmall_En

本文链接:https://www.cnblogs.com/BigSmall-En/p/18608841

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   BigSmall_En  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起