整体二分

例题

MKTHNUM - K-th Number

考虑如果我们对每个操作进行二分怎么做。

显然是对这个区间不大于二分值 \(mid\) 的数统计个数,看个数 \(num\)\(k\) 的大小关系。如果 \(num\) 更大,证明 \(mid\) 大了,如果 \(num\) 更小,证明 \(mid\) 小了。

然后我们考虑推广这种思路。

首先我们询问的答案肯定是原来数组中的一个数,所以我们可以直接把原数组排序,然后用下标代表其权值。这里我们的排序事实上可以用离散化完成,那样做的效果相同,但是排序更方便。

接下来,我们要实现整体二分的核心函数 \(solve(l,r,L,R)\),表示当前对于编号为 \(id,id\in[l,r]\) 的询问,可能的答案为 \([L,R]\) 内的一个数。

当然,给出的询问不会这么巧合正好是上面那种情况的,所以我们在整体二分时需要对询问排序,当然这个后面会说。

因为答案在 \([L,R]\) 内,所以考虑找到中点 \(mid\),然后把下标\([L,mid]\) 内的数加到树状数组里(事实上如果权值不大可以加权值,但这题只能使用下标,要不然空间会炸)。

然后考虑如果对于一个询问 \((l,r,k)\),树状数组在 \([l,r]\) 内的数的数量 \(num\) 不小于 \(k\),证明对于这个询问 \(mid\) 太大了,把他扔到数组 \(q1\) 中;否则就是 \(mid\) 太小了,于是先 \(k\leftarrow k-num\),然后把这个询问扔到数组 \(q2\) 内。

这里说一下为什么要 \(k\leftarrow k-num\)。因为我们接下来对这类 \(mid\) 太小了的询问会单独处理,而后面统计的是在新区间内不大于他的数的数量。我们不能忽略之前比他小的数带来的贡献,所以要做这样一个操作。

然后做完这个类似于归并排序的操作后,我们把之前树状数组里的东西清空掉,然后把 \(q1,q2\) 数组内的东西放回询问数组接着递归处理(事实上就是做了个分类,然后对两类询问继续递归)。

边界情况:\(L=R\),此时 \([l,r]\) 内的询问的答案结尾 \(val_L\)注意不是 \(L\),因为我们一开始用下标代替了权值

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
using namespace std;
int n,m,c[N],res[N];
struct node{
	int id,x;
	bool operator<(const node &t)const{
		return x<t.x;
	}
}a[N];
struct ask{
	int id,l,r,k;
}q[N],q1[N],q2[N];
int lowbit(int x){
	return x&-x;
}
void modify(int x,int v){
	while(x<=n){
		c[x]+=v;
		x+=lowbit(x);
	}
}
int qry(int x){
	int res=0;
	while(x){
		res+=c[x];
		x-=lowbit(x);
	}
	return res;
}
void solve(int l,int r,int L,int R){
	if(l>r)return;
	if(L==R){
		for(int i=l;i<=r;i++){
			res[q[i].id]=a[L].x;
		}
		return;
	}
	int mid=L+R>>1;
	for(int i=L;i<=mid;i++){
		modify(a[i].id,1);
	}
	int cnt1=0,cnt2=0;
	for(int i=l;i<=r;i++){
		int sum=qry(q[i].r)-qry(q[i].l-1);
		if(q[i].k<=sum){
			q1[++cnt1]=q[i];
		}
		else{
			q[i].k-=sum;
			q2[++cnt2]=q[i];
		}
	}
	for(int i=L;i<=mid;i++){
		modify(a[i].id,-1);
	}
	for(int i=1;i<=cnt1;i++){
		q[i+l-1]=q1[i];
	}
	for(int i=1;i<=cnt2;i++){
		q[i+l+cnt1-1]=q2[i];
	}
	solve(l,l+cnt1-1,L,mid);
	solve(l+cnt1,r,mid+1,R);
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i].x;
		a[i].id=i;
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r>>q[i].k;
		q[i].id=i;
	}
	sort(a+1,a+n+1);
	solve(1,m,1,n);
	for(int i=1;i<=m;i++){
		cout<<res[i]<<'\n';
	}
	return 0;
}

K大数查询

上一道题的动态版。这里说一句,如果没有第 \(c\) 大的数,输出 -1

注意这道题需要支持区间加区间查询,所以我们需要线段树或者两个树状数组进行维护。这里笔者采用了两个树状数组。

就比方说要求 \(\displaystyle\sum_{i=1}^{k}a_i\),我们设 \(d_i\) 为其差分数组。然后原式就是 \(\displaystyle\sum_{i=1}^{k}a_i\times (k-i+1)\),再化简一下就是 \(\displaystyle k\times\sum_{i=1}^{k}d_i-\sum_{i=1}^{k}(i-1)\times d_i\)。所以我们用两个树状数组维护前缀和即可

然后剩下的东西就跟上一道题一样。代码:

#include<bits/stdc++.h>
#define int long long
#define N 50005
using namespace std;
int n,m,res[N],c1[N],c2[N];
struct node{
	int op,l,r,c,id;
}q[N],q1[N],q2[N];
int lowbit(int x){
	return x&-x;
}
void bit_modify(int c[],int x,int v){
	while(x<=n){
		c[x]+=v;
		x+=lowbit(x);
	}
}
int bit_qry(int c[],int x){
	int res=0;
	while(x){
		res+=c[x];
		x-=lowbit(x);
	}
	return res;
}
void modify(int x,int v){
	bit_modify(c1,x,v);
	bit_modify(c2,x,v*(x-1));//这里是树状数组区间加区间查询模板
}
int qry(int x){
	return bit_qry(c1,x)*x-bit_qry(c2,x);//这里是树状数组区间加区间查询模板
}
void solve(int l,int r,int L,int R){
	if(l>r||L>R)return;
	if(L==R){
		for(int i=l;i<=r;i++){
			if(q[i].op==2){
				res[q[i].id]=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].c<=mid){
				q1[++cnt1]=q[i];
			}
			else{
				modify(q[i].l,1);
				modify(q[i].r+1,-1);
				q2[++cnt2]=q[i];
			}
		}
		else{
			int sum=qry(q[i].r)-qry(q[i].l-1);
			if(q[i].c<=sum){
				q2[++cnt2]=q[i];
			}
			else{
				q[i].c-=sum;
				q1[++cnt1]=q[i];
			}
		}
	}
	for(int i=l;i<=r;i++){
		if(q[i].op==1){
			if(q[i].c>mid){
				modify(q[i].l,-1);
				modify(q[i].r+1,1);
			}
		}
	}
	for(int i=1;i<=cnt1;i++){
		q[i+l-1]=q1[i];
	}
	for(int i=1;i<=cnt2;i++){
		q[i+l+cnt1-1]=q2[i];
	}
	solve(l,l+cnt1-1,L,mid);
	solve(l+cnt1,r,mid+1,R);
}
signed main(){
	cin>>n>>m;
	int cnt=0;
	for(int i=1;i<=m;i++){
		cin>>q[i].op>>q[i].l>>q[i].r>>q[i].c;
		if(q[i].op==2)q[i].id=++cnt;
	}
	solve(1,m,0,n);
	for(int i=1;i<=cnt;i++){
		if(res[i]==0)res[i]=-1;
		cout<<res[i]<<'\n';
	}
	return 0;
}

小马和路

本题可以使用 \(LCT\) 维护 \(MST\),但这里不再赘述。

考虑对于每条边,二分一个值,为如果以这条边作为最小边权的边加入所选边集,那么使得 \(1,n\) 连通所需加入的最大边权的边的边权最小能是多少。

发现二分 \(m\) 次时间过于爆炸,所以我们使用整体二分。

我们首先把待处理的边权倒序排序,然后每次使用可撤销并查集维护信息(不能路径压缩,故使用按秩合并),每次只需要扫一遍加边,判断加完边之后 \(1,n\) 是否连通,最后撤销即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 200005
#define pii pair<int,int>
#define x first
#define y second
using namespace std;
int n,m,fa[N],siz[N];
int q[N],q1[N],q2[N],res[N];
pii e[N];
stack<pii>s;
int find(int x){
	return fa[x]==x?x:find(fa[x]);
}
void merge(int x,int y){
	x=find(x);y=find(y);
	if(x==y)return;
	if(siz[x]<siz[y])swap(x,y);
	fa[y]=x;
	siz[x]+=siz[y];
	s.push({x,y});
}
void undo(){
	int x=s.top().x,y=s.top().y;
	s.pop();
	siz[x]-=siz[y];
	fa[y]=y;
}
void solve(int l,int r,int L,int R){
	if(l>r||L>R)return;
	if(L==R){
		for(int i=l;i<=r;i++){
			res[q[i]]=L;
		}
		return;
	}
	int mid=L+R>>1;
	int cnt1=0,cnt2=0;
	int now=mid+1;
	for(int i=r;i>=l;i--){
		if(q[i]>mid)q2[++cnt2]=q[i];
		else{
			while(now>q[i]){
				now--;
				merge(e[now].x,e[now].y);
			}
			if(find(1)==find(n))q1[++cnt1]=q[i];
			else q2[++cnt2]=q[i];
		}
	}
	while(!s.empty())undo();
	reverse(q1+1,q1+cnt1+1);
	reverse(q2+1,q2+cnt2+1);
	for(int i=1;i<=cnt1;i++)q[i+l-1]=q1[i];
	for(int i=1;i<=cnt2;i++)q[i+l-1+cnt1]=q2[i];
	solve(l,l+cnt1-1,L,mid);
	solve(l+cnt1,r,mid+1,R);
} 
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>e[i].x>>e[i].y;
		q[i]=i;
	}
	for(int i=1;i<=n;i++){
		fa[i]=i;
		siz[i]=1;
	}
	solve(1,m,1,m+1);
	int mn=2e18;
	for(int i=1;i<=m;i++){
		if(res[i]==m+1)continue;
		mn=min(mn,res[i]-i+1);
	}
	if(mn==2e18)mn=-1;
	cout<<mn;
	return 0;
}

作业

矩阵乘法

首先考虑显然第 \(k\) 小是可以二分的,但是我们发现每次都二分的时间复杂度很高,不能接受,于是使用整体二分。

首先使用经典技巧,用下标代替权值。每次我们二分一个值 \(mid\),然后把值在 \([L,mid]\) 内的数加入二维树状数组中,然后设 \(num\) 为二位树状数组中的数在查询 \(i\) 中的子矩阵的数量,于是分类讨论:

  • \(num\ge k_i\),归到左边。

  • 否则先 \(k_i\leftarrow k_i-num\),然后归到右边。

剩下的就是整体二分和二维树状数组的板子了,这里不多赘述。

混合果汁

首先还是先把 \(d\) 降序排序,然后把下标当作权值进行整体二分。

考虑二分如何进行判断。可以建立一棵以单价为下标的权值线段树,节点存储单价在 \([l,r]\) 的所有果汁的总体积与总价格(注意区分单价与总价)。

然后就是简单的了,每次只需要先判断总体积够不够。如果够,考虑权值线段树上二分,查找在体积够的情况下的最小花费(显然选单价更低的更优)。

posted @ 2024-07-27 12:37  zxh923  阅读(9)  评论(0编辑  收藏  举报