Loading

【笔记】二分

二分分为二分查找和二分答案。

二分查找

每次查询或询问的结果:

  • 找到,结束;
  • 找不到,答案所在范围缩小一半。如果小了就查找大的那一半,如果大了就查找小的那一半。

查找次数为对数级。

前提:序列有序。

二分查找可以解决的问题:

  • 大于等于 \(x\) 的最小值是哪个?大于等于 \(x\) 的有多少个?
  • 小于等于 \(x\) 的最大值是哪个?小于等于 \(x\) 的有多少个?
  • 如果多个值与 \(x\) 相等,有多少个?
  • 如果找不到 \(x\),与 \(x\) 最接近的是几?

模板题:Luogu P2249 查找

此题有虽然有序,但是有相同元素,如果每次查找 \(a[mid]\) $\color{red} \ge $ 目标那么 \(R=mid\),不能舍弃相同的,否则 \(L=mid+1\),直接跳过所有比目标小的。

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=1e6+10;
int n,m,q;
int a[maxn];
int bfind(int tar){
	int l=1,r=n,mid;
	while(l<r){
		mid=(l+r)/2;
		if(a[mid]>=tar) r=mid;
		else l=mid+1;
	}
	if(a[l]==tar) return l;
	return -1;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=m;i++){
		scanf("%d",&q);
		printf("%d ",bfind(q));
	}
	return 0;
}

这个二分查找中,如果找到了那么 \(L,R,mid\) 将会指向同一个位置,找一个输出即可。

变式:小于等于 \(x\) 中的最大值(如果多个和 \(x\) 相等,返回最后一个)。

如果 \(a[mid]\) \(\color{red} \le\) 目标那么 \(l=mid\),否则 \(r=mid-1\)

但是这样会出现死循环,因为下面的 \(mid-1\) 了所以执行一万次 \(l\) 也都会小于 \(r\),因此上面要把 \(1\) 加回来,\(mid=\frac{l+r+1}{2}\)

模板总结

找大于等于 \(x\) 的下标最小值:\(a_{mid} \ge x\)\(r=mid\)\(a_{mid} < x\)\(l=mid+1\)

int bfind(int tar){
	int l=1,r=n,mid;
	while(l<r){
		mid=(l+r)/2;
		if(a[mid]>=tar) r=mid;
		else l=mid+1;
	}
	if(a[l]==tar) return l;
	else return -1;
}

找小于等于 \(x\) 的下标最大值:\(a_{mid} \le x\)\(l=mid\)\(a_{mid} > x\)\(r=mid-1\),同时 \(mid=(l+r+1)/2\)

int bfind(int tar){
	int l=1,r=n,mid;
	while(l<r){
		mid=(l+r+1)/2;
		if(a[mid]<=tar) l=mid;
		else r=mid-1;
	}
	if(a[l]==tar) return l;
	else return -1;
}

Luogu P1102 A-B 数对

遍历每个 \(a_i\),找 \(a_i+c\) 出现了几次。

出现了几次可以用两种方法解决:

  • 分别找下标最小值和最大值(上文)
  • 用桶记录

注意不是有序的,记得 sort

极端数据下答案会到 \(10^{10}\),需要开 long long

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int maxn=2e5+10;
int n,c;
long long cnt;
int a[maxn];
int bfindmin(int tar){
	int l=1,r=n,mid;
	while(l<r){
		mid=(l+r)/2;
		if(a[mid]>=tar) r=mid;
		else l=mid+1;
	}
	if(a[l]==tar) return l;
	else return -1;
}
int bfindmax(int tar){
	int l=1,r=n,mid;
	while(l<r){
		mid=(l+r+1)/2;
		if(a[mid]<=tar) l=mid;
		else r=mid-1;
	}
	if(a[l]==tar) return l;
	else return -1;
}
int main(){
	scanf("%d%d",&n,&c);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		int ans1,ans2;
		ans1=bfindmin(a[i]+c);
		if(ans1!=-1){
			ans2=bfindmax(a[i]+c);
			cnt+=(ans2-ans1+1);
		}		
	}
	cout<<cnt;
	return 0;
}

二分答案

二分答案:一些东西加起来的值需要满足某个条件,求其中的最大值最小是多少或最小值最大是多少。

Luogu P1873 [COCI 2011/2012 #5] EKO / 砍树

\(1\) 到最高树的高度进行二分锯子的高度答案,因为要求锯片尽可能高所以套模板二。

#include<iostream>
#include<cstdio>
#define int long long
using namespace std;
const int maxn=1e6+10;
int n,m,maxx;
int a[maxn];
int bfind(){
	int l=1,r=maxx,mid,sum;
	while(l<r){
		sum=0;
		mid=(l+r+1)/2;
		for(int i=1;i<=n;i++){
			if(a[i]>mid)sum+=a[i]-mid;
		}
		if(sum>=m) l=mid;
		else r=mid-1;
	}
	return l;
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		maxx=max(maxx,a[i]);
	}
	printf("%lld\n",bfind());
	return 0;
} 

Luogu P2440 木材加工

把上题代码的-改成/就过了。

#include<iostream>
#include<cstdio>
#define int long long
using namespace std;
const int maxn=1e5+10;
int n,k,maxx;
int a[maxn];
int bfind(){
	int l=0,r=maxx,mid,sum;
	while(l<r){
		sum=0;
		mid=(l+r+1)/2;
		for(int i=1;i<=n;i++){
			if(a[i]>mid)sum+=a[i]/mid;
		}
		if(sum>=k) l=mid;
		else r=mid-1;
	}
	return l;
}
signed main(){
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		maxx=max(maxx,a[i]);
	}
	printf("%lld\n",bfind());
	return 0;
} 

Luogu P1314 [NOIP2011 提高组] 聪明的质监员

题目比较绕,给了两个数学公式。我们二分 \(W\),然后分别按公式求解每个询问的答案。这里要注意,二分的时候因为不知道 \(y\)\(s\) 大是正确答案还是 \(y\)\(s\) 小是正确答案,所以可以在每次二分的时候记录 \(|y-s|\) 的最小值,或者在最后输出的时候再去查一下另一种情况。并且在求解数学公式的时候,爆搜的复杂度是 \(O(n^2 \log w)\),过不了,可以发现题目中的数学公式全部是求和,所以需用前缀和优化为 \(O(n \log w)\)

#include<iostream>
#include<cmath>
#include<cstdio>
#define int long long
using namespace std;
const int maxn=2e5+10;
const int maxs=1e18;
struct mine{
	int w,v;
}a[maxn];
int n,m,s;
int prv[maxn],prn[maxn],L[maxn],R[maxn];
int check(int mid){
	int y=0;
	for(int i=1;i<=n;i++){
		if(a[i].w>=mid){
			prn[i]=prn[i-1]+1;
			prv[i]=prv[i-1]+a[i].v;
		}else{
			prn[i]=prn[i-1];
			prv[i]=prv[i-1];
		}
	}
	for(int i=1;i<=m;i++){
		y+=(prn[R[i]]-prn[L[i]-1])*(prv[R[i]]-prv[L[i]-1]);
	}
	return y;
}
int bfind(){
	int l=0,r=1000000,mid,ans=maxs;
	while(l<r){
		mid=(l+r+1)/2;
		int t=check(mid);
		if(t>=s) l=mid;
		else r=mid-1;
		if(abs(s-t)<ans) ans=abs(s-t);
	}
	return ans;
}
signed main(){
	scanf("%lld%lld%lld",&n,&m,&s);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&a[i].w,&a[i].v);
	}
	for(int i=1;i<=m;i++){
		scanf("%lld%lld",&L[i],&R[i]);
	}
	printf("%lld",bfind());
	return 0;
}

实数二分

与整数二分类似,但有几个点要注意:

  • \(l<r\) 会死循环,正确写法:设题目要求保留 \(k\) 位,\(l<r-10^{-k-2}\)
  • \(l=mid+1\) 会跳过大量实数,正确写法:\(l=mid+10^{-k-2}\)
  • \(mid-x>=0\) 有时会因为精度问题导致不能正确判断,正确写法:\(mid-x>=10^{-k-2}\)
  • 如果是二分开方,要注意在 \((0,1)\) 之间的情况。

例:求算术平方根二分

正确写法 1:

#include<iostream>
using namespace std;
double x;
int main(){
	cin>>x;
	double l=-100000,r=100000,mid;
	while(l<r-0.0001){
		mid=(l+r)/2.0;
		if(mid*mid-x>=0) r=mid;
		else l=mid;
	}
	printf("%.2lf",l);
	return 0;
}

正确写法 2(此写法能更好避免精度问题):

#include<iostream>
using namespace std;
double x;
int main(){
	cin>>x;
	double l=-100000,r=100000,mid;
	for(int i=1;i<=100;i++){
		mid=(l+r)/2;
		if(mid*mid-x>=0) r=mid;
		else l=mid;
	}
	printf("%.2lf",l);
	return 0;
}

相当于把 \([l,r]\) 划分成 \(2^{100} \approx 10^{30}\) 段,把循环次数固定,避免了 \(l<r-0.0001\) 这样写的精度问题。一般来说,循环 \(40\) 次的精度就足够了。

posted @ 2025-12-12 23:17  Seqfrel  阅读(0)  评论(0)    收藏  举报