【学习笔记】整体二分

一、简述

叫简述是因为不知道该叫啥。

整体二分是一种基于值域的分治算法,一般面对多次可二分的询问时就可以整体二分。有时带修改的问题也可以整体二分,但显然它难以强制在线。

整体二分的精髓在于对于一堆问题只 check 一次,从而节省相当多的时间。

二、例题

1.Luogu P3834 【模板】可持久化线段树 2

考虑如何高效地确定一些询问的答案与当前 \(mid\) 的关系。

考虑树状数组,将 \([l,mid]\) 中的数全都插入到对应的位置上。于是对于每个询问,就可以用树状数组来求出询问区间中 \([l,mid]\) 的数量。于是就好做了。时间复杂度 \(O(n\log^2n)\),需要离散化。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=2e5+5;
//int cnt;
int n,m,a[maxn],lsh[maxn],tot,ans[maxn];
struct{
	int tr[maxn];
	il int lowbit(int x){
		return x&-x;
	}
	il void add(int p,int v){
		for(;p<=n;p+=lowbit(p)){
			tr[p]+=v;
		}
	}
	il int query(int p){
		int res=0;
		for(;p;p-=lowbit(p)){
			res+=tr[p];
		}
		return res;
	}
}F;
struct node1{
	int p,v;
	node1(int p=0,int v=0):p(p),v(v){}
};
struct node2{
	int l,r,k,id;
	node2(int l=0,int r=0,int k=0,int id=0):l(l),r(r),k(k),id(id){}
};
il void solve(int l,int r,vector<node1> p,vector<node2> q){
//	cerr<<l<<" "<<r<<" "<<p.size()<<" "<<q.size()<<"\n";
	if(l==r){
		for(node2 i:q){
			ans[i.id]=lsh[l];
		}
		return ;
	}
	int mid=(l+r)>>1;
	vector<node1> p1,p2;
	for(node1 i:p){
		if(i.v<=mid){
			F.add(i.p,1);
			p1.pb(i);
		}
		else{
			p2.pb(i);
		}
	}
	vector<node2> q1,q2;
	for(node2 &i:q){
//		cnt++;
		int tmp=F.query(i.r)-F.query(i.l-1);
		if(i.k<=tmp){
			q1.pb(i);
//			cerr<<"1 ";
		}
		else{
			i.k-=tmp;
			q2.pb(i);
		}
	}
//	cerr<<"\n"<<q1.size()<<"\n";
	for(node1 i:p){
		if(i.v<=mid){
			F.add(i.p,-1);
		}
	}
	solve(l,mid,p1,q1);
	solve(mid+1,r,p2,q2);
}
int main(){
//	freopen("P3834_6.in","r",stdin);
//	freopen("my.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		lsh[++tot]=a[i];
	}
	sort(lsh+1,lsh+tot+1);
	tot=unique(lsh+1,lsh+tot+1)-lsh-1;
	vector<node1> p;
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(lsh+1,lsh+tot+1,a[i])-lsh;
		p.pb(node1(i,a[i]));
	}
	vector<node2> q;
	for(int i=1,l,r,k;i<=m;i++){
		cin>>l>>r>>k;
		q.pb(node2(l,r,k,i));
	}
	solve(1,tot,p,q);
//	cerr<<cnt<<"\n";
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

2.[bzoj1901][Zju2112] Dynamic Rankings

首先可以将更改操作改为一个删除操作再加一个插入操作。将所有的操作和查询存在一个操作序列中进行分治。对于当前的 \(mid\),如果一个修改操作要修改的数大于 \(mid\),那么它对左区间就不会有影响,直接放入右区间。否则就将贡献加入树状数组,放入左区间即可。注意维持操作序列的时间顺序。

实际上是可以不离散化的,对时间复杂度的影响不大,但要注意必须特判掉空的操作序列区间,否则就会跑满整个 dfs 树,这样的时间复杂度是无法承受的。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,a[maxn],cnt,ans[maxn];
struct node{
	int typ,l,k,r,id;
	node(int typ=0,int l=0,int k=0,int r=0,int id=0)
		:typ(typ),l(l),k(k),r(r),id(id){}
}b[maxn*3],hp[maxn*3];
struct{
	int tr[maxn];
	il int lowbit(int x){
		return x&-x;
	}
	il void add(int p,int v){
		for(;p<=n;p+=lowbit(p)){
			tr[p]+=v;
		}
	}
	il int query(int p){
		int res=0;
		for(;p;p-=lowbit(p)){
			res+=tr[p];
		}
		return res;
	}
}F;
il void solve(int l,int r,int L,int R){
	if(l>r){
		return ;
	}
//	if(l!=10||r!=9){
//		cout<<l<<" "<<r<<" "<<L<<" "<<R<<"\n";
//	}
	if(L==R){
		for(int i=l;i<=r;i++){
			ans[b[i].id]=L;
		}
		return ;
	}
	int pl=l,pr=r;
	int mid=(L+R)>>1;
	for(int i=l;i<=r;i++){
		if(b[i].typ){
			if(b[i].k<=mid){
				F.add(b[i].l,b[i].typ);
				hp[pl++]=b[i];
			}
			else{
				hp[pr--]=b[i];
			}
		}
		else{
			int tmp=F.query(b[i].r)-F.query(b[i].l-1);
			if(tmp>=b[i].k){
				hp[pl++]=b[i];
			}
			else{
				b[i].k-=tmp;
				hp[pr--]=b[i];
			}
		}
	}
	for(int i=l;i<=r;i++){
		if(b[i].typ&&b[i].k<=mid){
			F.add(b[i].l,-b[i].typ);
		}
	}
	for(int i=l;i<pl;i++){
		b[i]=hp[i];
	}
	for(int i=pl,j=r;i<=r;i++,j--){
		b[i]=hp[j];
	}
	solve(l,pr,L,mid);
	solve(pl,r,mid+1,R);
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		b[++cnt]=node(1,i,a[i]);
	}
	for(int i=1;i<=m;i++){
		char typ;
		cin>>typ;
		if(typ=='Q'){
			int l,r,k;
			cin>>l>>r>>k;
			b[++cnt]=node(0,l,k,r,i);
		}
		else{
			int p,v;
			cin>>p>>v;
			b[++cnt]=node(-1,p,a[p]);
			b[++cnt]=node(1,p,v);
			a[p]=v;
			ans[i]=-1;
		}
	}
	solve(1,cnt,0,1e9);
	for(int i=1;i<=m;i++){
		if(~ans[i]){
			cout<<ans[i]<<"\n";
		}
	}
	return 0;
}
}
int main(){return asbt::main();}

3.[国家集训队] 矩阵乘法

依然是区间第 \(k\) 大,只不过改成了矩阵,使用二维树状数组即可。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace IO{
	const int bufsz=1<<20;
	char ibuf[bufsz],*p1=ibuf,*p2=ibuf;
	#define getchar() (p1==p2&&(p2=(p1=ibuf)+fread(ibuf,1,bufsz,stdin),p1==p2)?EOF:*p1++)
	il int read(){
		char ch=getchar();
		while(ch<'0'||ch>'9'){
			ch=getchar();
		}
		int x=0;
		while(ch>='0'&&ch<='9'){
			x=(x<<1)+(x<<3)+(ch^48);
			ch=getchar();
		}
		return x;
	}
	#undef getchar
}
using IO::read;
const int maxn=6e4+5,maxm=3.2e5+5;
int n,m,cnt,a[505][505],ans[maxn];
struct{
	int tr[505][505];
	il int lowbit(int x){
		return x&-x;
	}
	il void add(int x,int y,int v){
		for(int i=x;i<=n;i+=lowbit(i)){
			for(int j=y;j<=n;j+=lowbit(j)){
				tr[i][j]+=v;
			}
		}
	}
	il int query(int x,int y){
		int res=0;
		for(int i=x;i;i-=lowbit(i)){
			for(int j=y;j;j-=lowbit(j)){
				res+=tr[i][j];
			}
		}
		return res;
	}
}F;
struct node{
	int x1,y1,k,x2,y2,id;
	node(int x1=0,int y1=0,int k=0,int x2=0,int y2=0,int id=0)
		:x1(x1),y1(y1),x2(x2),y2(y2),k(k),id(id){}
}b[maxm],hp[maxm];
il void solve(int l,int r,int L,int R){
	if(l>r){
		return ;
	}
	if(L==R){
		for(int i=l;i<=r;i++){
			ans[b[i].id]=L;
		}
		return ;
	}
	int mid=(L+R)>>1,pl=l,pr=r;
	for(int i=l;i<=r;i++){
		if(b[i].id){
			int x1=b[i].x1,y1=b[i].y1,x2=b[i].x2,y2=b[i].y2;
			int tmp=F.query(x2,y2)-F.query(x1-1,y2)-F.query(x2,y1-1)+F.query(x1-1,y1-1);
			if(tmp>=b[i].k){
				hp[pl++]=b[i];
			}
			else{
				b[i].k-=tmp;
				hp[pr--]=b[i];
			}
		}
		else{
			if(b[i].k<=mid){
				F.add(b[i].x1,b[i].y1,1);
				hp[pl++]=b[i];
			}
			else{
				hp[pr--]=b[i];
			}
		}
	}
	for(int i=l;i<=r;i++){
		if(!b[i].id&&b[i].k<=mid){
			F.add(b[i].x1,b[i].y1,-1);
		}
	}
	for(int i=l;i<pl;i++){
		b[i]=hp[i];
	}
	for(int i=pl,j=r;i<=r;i++,j--){
		b[i]=hp[j];
	}
	solve(l,pr,L,mid);
	solve(pl,r,mid+1,R);
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			a[i][j]=read();
			b[++cnt]=node(i,j,a[i][j]);
		}
	}
	for(int i=1,x1,y1,x2,y2,k;i<=m;i++){
		x1=read(),y1=read(),x2=read(),y2=read(),k=read();
		b[++cnt]=node(x1,y1,k,x2,y2,i);
	}
	solve(1,cnt,0,1e9);
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

4.[THUPC2017] 天天爱射击

整体二分,将每个木板被击碎的时间二分出来即可。注意有的木板可能根本无法击穿。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e5+5;
int n,m,a[maxn],ans[maxn];
struct node{
	int x1,x2,k;
}b[maxn],hp[maxn];
struct{
	int tr[maxn];
	il int lowbit(int x){
		return x&-x;
	}
	il void add(int p,int v){
		for(;p<=2e5;p+=lowbit(p)){
			tr[p]+=v;
		}
	}
	il int query(int p){
		int res=0;
		for(;p;p-=lowbit(p)){
			res+=tr[p];
		}
		return res;
	}
}F;
il 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(b[i].x1<=a[L]&&b[i].x2>=a[L]&&b[i].k==1){
				ans[L]++;
			}
		}
		return ;
	}
	int mid=(L+R)>>1,pl=l,pr=r;
	for(int i=L;i<=mid;i++){
		F.add(a[i],1);
	}
	for(int i=l;i<=r;i++){
		int tmp=F.query(b[i].x2)-F.query(b[i].x1-1);
		if(tmp>=b[i].k){
			hp[pl++]=b[i];
		}
		else{
			b[i].k-=tmp;
			hp[pr--]=b[i];
		}
	}
	for(int i=L;i<=mid;i++){
		F.add(a[i],-1);
	}
	for(int i=l;i<pl;i++){
		b[i]=hp[i];
	}
	for(int i=pl,j=r;i<=r;i++,j--){
		b[i]=hp[j];
	}
	solve(l,pr,L,mid);
	solve(pl,r,mid+1,R);
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>b[i].x1>>b[i].x2>>b[i].k;
	}
	for(int i=1;i<=m;i++){
		cin>>a[i];
	}
	solve(1,n,1,m);
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

5.「CTSC2018」混合果汁

考虑对于单次询问,显然可以二分,每次将美味度大于等于 \(mid\) 的果汁放在一起按价格排序,贪心地计算能否在 \(g_j\) 的限制内买够 \(L_j\) 的果汁。

多次询问可二分的问题,这提醒我们去做整体二分。但是有一个问题,我们无法在整体二分搜索树上的每个节点都对大于等于 \(mid\) 的果汁进行排序计算,不然显然会炸。有一个解决方案是,将一堆没有交集的搜索树节点放在一起计算,用线段树维护按美味度排序后的某个后缀果汁集合。具体地,按照美味度倒着扫,对于每个美味度在 \([L,R]\) 的搜索树节点,我们先将 \([mid+1,R]\) 的果汁加入线段树,然后处理节点上的询问,再将 \([L,mid]\) 的果汁加入线段树。换言之,我们要按层遍历搜索树,并且每一层还要倒着遍历!一个方案是用 std::vector 存储当前层的搜索树节点写 bfs。时间复杂度 \(O((n+m)\log^2n)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,lsh[maxn];
ll ans[maxn];
struct jui{
	int d,p,l;
}a[maxn];
struct{
	int id;
	ll g,l;
}b[maxn],hp[maxn];
int rt,tot;
struct{
	int ls,rs,d,p,l;
	ll smp,sml;
}tr[maxn<<1];
#define ls(id) tr[id].ls
#define rs(id) tr[id].rs
#define d(id) tr[id].d
#define p(id) tr[id].p
#define l(id) tr[id].l
#define smp(id) tr[id].smp
#define sml(id) tr[id].sml
il void pushup(int id){
	smp(id)=smp(ls(id))+smp(rs(id));
	sml(id)=sml(ls(id))+sml(rs(id));
}
il void insert(int &id,int L,int R,int p,int d,int l){
	if(!id){
		id=++tot;
		ls(id)=rs(id)=0;
		d(id)=p(id)=l(id)=0;
		smp(id)=sml(id)=0;
	}
	if(L==R){
		smp(id)=lsh[p]*1ll*l;
		sml(id)=l;
		d(id)=d,p(id)=lsh[p],l(id)=l;
		return ;
	}
	int mid=(L+R)>>1;
	if(p<=mid){
		insert(ls(id),L,mid,p,d,l);
	}
	else{
		insert(rs(id),mid+1,R,p,d,l);
	}
	pushup(id);
}
il ll query(int id,int L,int R,ll g){
	if(!id){
		return 0;
	}
	if(L==R){
		return min(g/p(id),l(id)*1ll);
	}
	int mid=(L+R)>>1;
	if(smp(ls(id))>=g){
		return query(ls(id),L,mid,g);
	}
	else{
		return query(rs(id),mid+1,R,g-smp(ls(id)))+sml(ls(id));
	}
}
#undef ls
#undef rs
#undef d
#undef p
#undef l
#undef smp
#undef sml
struct node{
	int L,R,l,r;
	node(int L=0,int R=0,int l=0,int r=0):L(L),R(R),l(l),r(r){}
};
vector<node> q[2];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i].d>>a[i].p>>a[i].l;
	}
	sort(a+1,a+n+1,[](const jui &x,const jui &y){return x.p<y.p;});
	for(int i=1;i<=n;i++){
		lsh[i]=a[i].p;
		a[i].p=i;
	}
	sort(a+1,a+n+1,[](const jui &x,const jui &y){return x.d>y.d;});
	for(int i=1;i<=m;i++){
		cin>>b[i].g>>b[i].l;
		b[i].id=i;
	}
	q[0].pb(node(1,1e5,1,m));
	int cur=0;
	while(q[cur].size()){
		q[cur^1].clear();
		int now=1;
		rt=tot=0;
		for(node x:q[cur]){
			int L=x.L,R=x.R,l=x.l,r=x.r;
//			cout<<L<<" "<<R<<":\n";
//			for(int i=l;i<=r;i++){
//				cout<<b[i].id<<" ";
//			}
//			puts("");
			if(l>r){
				continue;
			}
			if(L==R){
				while(now<=n&&a[now].d>=L){
					insert(rt,1,n,a[now].p,a[now].d,a[now].l);
					now++;
				}
				for(int i=l;i<=r;i++){
					if(query(rt,1,n,b[i].g)>=b[i].l){
						ans[b[i].id]=L;
					}
					else{
						ans[b[i].id]=-1;
					}
				}
				continue;
			}
			int mid=(L+R)>>1;
			while(now<=n&&a[now].d>mid){
				insert(rt,1,n,a[now].p,a[now].d,a[now].l);
				now++;
			}
			int pl=l,pr=r;
			for(int i=l;i<=r;i++){
				if(query(rt,1,n,b[i].g)>=b[i].l){
					hp[pr--]=b[i];
				}
				else{
					hp[pl++]=b[i];
				}
			}
			for(int i=l;i<pl;i++){
				b[i]=hp[i];
			}
			for(int i=pl;i<=r;i++){
				b[i]=hp[i]; 
			}
			q[cur^1].pb(node(mid+1,R,pl,r));
			q[cur^1].pb(node(L,mid,l,pr));
			while(now<=n&&a[now].d>=L){
				insert(rt,1,n,a[now].p,a[now].d,a[now].l);
				now++;
			}
		}
		cur^=1;
	}
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}
posted @ 2025-06-22 17:24  zhangxy__hp  阅读(10)  评论(0)    收藏  举报