二分习题补充

L-Coding于2025.8.2——2025.8.3作。

一、浮点二分

※ 注意事项

  • 维护double类型指针;
  • 修改指针时不应加减1
  • while循环条件应为r-l>delta,其中delta为误差,一般设置为形如1e-41e-6的浮点数,具体情况视题目而定。

1. P3743 小鸟的设备

如果枚举时间和设备,用模拟法求最大使用时长,由于本题中时间为浮点型,范围太大,枚举模拟一定会超时。

不妨分析答案单调性:如果最大使用时长是ans,那么在比ans小的使用时长内,所有设备一定能连续工作;在比ans大的使用时长内,一定存在某个设备的能量会在ans时刻降为0。所以,对于所有的使用时长x的合法性check(x),可以画出一个数轴,使得数轴上表示check(ans)这一点及其左侧全部为true,右侧[不包含check(ans)]则全部为false。因此,答案是单调的,考虑使用二分答案。

综上所述,「求答案」的模拟做法,和二分答案「判断答案是否可行」的做法,在答案的来源层面,是完全相反的。前者更偏向于「计算」,后者更偏向于「试数」。已知答案属于一个单调区间,更好的做法显然是二分答案。

考虑判断答案x是否可行的算法。逆向思考,如果答案x可行,那么充电器在x时间内最多能提供的电量maxc,一定大于或等于在x时间内所有电器运行需要充电器补充的电量sum。于是,考虑枚举设备,如果某一设备的功率a[i]与答案x的乘积(即这一设备在答案x内消耗的电能)大于这一设备原先储存的电能b[i],那么就需要把sum自增前两者之差(即这一设备在答案x内保持工作需要充电器额外供给的电能)。

再考虑二分写法。左右边界可以设为01e10(数据上限),通过题中给出的允许误差Δans=1e-4,可以写出while循环的条件为r-l>1e-4。值得注意的是,这是一道浮点二分答案,左右指针都是浮点型,当它们需要移动时,必须将其直接设置为中点指针mid,而不采取加或减1的操作。

注意到题中的特判。不难看出,当所有设备的功率之和小于或等于充电器的功率时,设备就能无限使用。

由上,可写出:

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,p,a[N],b[N];
double tot=0;
bool check(double x){
	double maxc=x*p;
	double sum=0;
	for(int i=1;i<=n;i++){
		if(a[i]*x>b[i]){
			sum+=a[i]*x-b[i];
		}
	}
	return maxc>=sum;
}
int main(){
	cin>>n>>p;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i];
		tot+=a[i];
	}
	if(tot<=p){
		cout<<-1<<endl;
		return 0;
	}
	double l=0,r=1e10,ans=0;
	while(r-l>1e-4){
		double mid=(l+r)/2;
		if(check(mid)){
			l=mid;
		}else{
			r=mid;
		}
	}
	cout<<l<<endl;
	return 0;
}

2. P1163 银行贷款

我数学不好。

简单来说,这道题就是求式

\[\sum_{i=1}^m \frac{w}{(1+ans)^i}=w_0 \]

\(ans\)的值。

考虑模拟原求和式的计算过程,模拟法实现即可,不多赘述。

注意到题中表明\(ans\)存在一个范围,如果\(ans\)单调,那么就可以用二分答案得出答案。不妨分析答案单调性。当\(ans\)变大时,分式分母变大,分式变小,原式变小,故原求和式单调递减。

于是我们的check(x)函数同样返回布尔类型,但返回值却反映原求和式的值sumw0的大小关系(大于或小于),即返回sum>w0

值得注意的是,二分答案时,当check(mid)等于1时,应该搜索的是mid的右半部分,而非左半部分。这是因为,mid相当于式中的\(ans\),当搜到一个\(ans\),使得这个\(ans\)经过运算所得的式值大于目标值时,表明式值过大,需要让式值变小。如果式值变小,那么分式也应变小,分式分母就应变大,\(ans\)就应变大,于是就应搜索比mid大的答案。

本题仍然是浮点二分,维护指针时也应注意不应加减1。通过笔者的调试,误差应设置为1e-4

代码:

#include <bits/stdc++.h>
using namespace std;
double w0,w;
int m;
bool check(double x){
	double sum=0,rate=1;
	for(int i=1;i<=m;i++){
		rate*=1+x;
		sum+=w/rate;
	}
	return sum>w0;
}
int main(){
	scanf("%lf%lf%d",&w0,&w,&m);
	double l=0,r=3,ans=0;
	while(r-l>1e-4){
		double mid=(l+r)/2;
		if(check(mid)){
			l=mid;
		}else{
			r=mid;
		}
	}
	printf("%.1lf",l*100);
	return 0;
}

3. P1024 [NOIP 2001 提高组] 一元三次方程求解

(1) 枚举

数据太水,枚举可以AC。模拟即可。枚举根(double类型)的范围,根数为3时退出。

代码:

#include <bits/stdc++.h>
using namespace std;
double a,b,c,d;
int main(){
	int roots=0;
	scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
	for(double i=-100.000;i<=100.000;i+=0.001){
		if(fabsl(i*i*i*a+i*i*b+i*c+d)<1e-5){
			printf("%.2lf ",i);
			roots++;
		}
		if(roots==3)return 0;
	}
}

补充:fabsl()函数可以求double类型的绝对值,请牢记。

(2) 二分答案

高中数学《函数》一章曾提到函数零点存在定理

对于连续函数\(f(x)\),若\(f(x_1)f(x_2)<0\),且\(x_1<x_2\),则方程\(f(x)=0\)在区间\((x_1,x_2)\)之间有且至少有一个根。

这对于题中的三次函数同样适用。

题中表明,根与根之差的绝对值大于或等于1。因此我们可以枚举区间\([-100,100]\)内的所有\([i,i+1)\),根据函数零点存在定理,如果这一左闭右开区间内有根,那么就可以二分答案这一区间内的根。

那么,如何判断中点与根的大小关系呢?下面列举表明了根与中点的相对大小与中点正负、函数增减性的关系(若无特殊说明,以下的"中点/左/右指针"均指以中点/左/右指针为自变量的函数值):

函数在区间内的增减性 中点与根之间的较小者 中点与根之间的较大者
单调递增(左指针负,右指针正) 中点(正)
单调递增(左指针负,右指针正) 中点(负)
单调递减(左指针正,右指针负) 中点(正)
单调递减(左指针正,右指针负) 中点(负)

观察上表,不难得出:当中点与左指针同号时,根较大;当中点与左指针异号时,根较小。

即:若\(f(mid)f(l)>0\),则搜索\(mid\)右侧;反之,则搜索其左侧。

需要注意,如果l为根,直接输出l;必须使r不为根,才能二分答案(否则WA俩点)。

由此:

#include <bits/stdc++.h>
using namespace std;
double a,b,c,d;
double f(double x){
	return a*x*x*x+b*x*x+c*x+d;
}
int main(){
	cin>>a>>b>>c>>d;
	for(int i=-100;i<=100;i++){
		double l=i,r=i+1;
		if(fabs(f(l))<1e-4){
			printf("%.2lf ",l);
		}else if(f(l)*f(r)<0 && fabs(f(r))>=1e-4){
			while(r-l>1e-4){
				double mid=(l+r)/2;
				if(f(mid)*f(r)>0){
					r=mid;
				}else{
					l=mid;
				}
			}
			printf("%.2lf ",l);
		}
	}
	return 0;
}

4. P1577 切绳子

本题与P2440 木材加工基本一致,只是增加了精度,故不多赘述。

using namespace std;
const int N=1e4+10;
int n,k;
double a[N];
bool check(double x){
	int sum=0;
	for(int i=1;i<=n;i++){
		sum+=a[i]/x;
	}
	return sum>=k;
}
int main(){
	double l=0,r=0;
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		r+=a[i];
	}
	while(r-l>1e-4){
		double mid=(l+r)/2;
		if(check(mid)){
			l=mid;
		}else{
			r=mid;
		}
	}
	printf("%.2f",l);
	return 0;
}

二、二分综合

1. P1182 数列分段 Section II

本题求"最小的最大值",考虑二分答案。

判断答案是否可行的方法:维护求和变量sum,遍历数组元素累加求和(条件为sum小于或等于待判断答案),否则计为数列的一段。如果段计数器小于或等于目标段数则答案可行,反之则不可行。注意段计数器cnt初始值应为1。

二分答案时,左指针应为数列中最大的元素,右指针应为数列所有元素之和。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,a[N];
bool check(int x){
	int sum=0,cnt=1;
	for(int i=1;i<=n;i++){
		if(sum+a[i]<=x){
			sum+=a[i];
		}else{
			sum=a[i];
			cnt++;
		}
	}
	return cnt<=m;
}
int main(){
	cin>>n>>m;
	int l=INT_MIN,r=0,ans;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		l=max(l,a[i]);
		r+=a[i];
	}
	while(l<=r){
		int mid=l+r>>1;
		if(check(mid)){
			ans=mid;
			r=mid-1;
		}else{
			l=mid+1;
		}
	}
	cout<<ans;
	return 0;
}

2. P1281 书的复制

本题与P1182 数列分段 Section II基本一致,只是增加了几个限制,输出也做了适当改动。

判断答案是否可行的方法不多赘述。

对于输出,由于起始编号和终止编号成对出现,且起始编号从小到大排列,故考虑维护一个pair<int,int>类型的vector容器,存储编号对。每个编号对的终止编号在起始编号之前确定,注意倒序遍历,倒序输出。

#include <bits/stdc++.h>
using namespace std;
const int N=505;
int n,m,a[N];
bool check(int x){
	int sum=0,cnt=0;
	for(int i=1;i<=n;i++){
		if(sum+a[i]<=x){
			sum+=a[i];
		}else{
			sum=a[i];
			cnt++;
		}
	}
	return cnt<m;
}
void print(int x){
	vector<pair<int,int>>ans;
	int sum=0,last=n;
	for(int i=n;i>=1;i--){
		if(sum+a[i]>x){
			ans.push_back({i+1,last});
			last=i;
			sum=a[i];
		}else{
			sum+=a[i];
		}
	}
	ans.push_back({1,last});
	for(int i=ans.size()-1;i>=0;i--){
		cout<<ans[i].first<<" "<<ans[i].second<<endl;
	}
}
int main(){
	int l=0,r=1e9,ans;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		l=max(l,a[i]);
		r+=a[i];
	}
	while(l<=r){
		int mid=l+r>>1;
		if(check(mid)){
			ans=mid;
			r=mid-1;
		}else{
			l=mid+1;
		}
	}
	print(ans);
	return 0;
}

3. P1396 营救

注:本题做法很多,这里仅提供二分+BFS做法。

二分答案最小的最大拥挤度,从起点s广搜,在边权小于或等于答案的情况下,若搜到终点,则证明答案可行。

警示后人:①不要开邻接矩阵,会MLE,必须用邻接表;②有重边和自环,必须特判。

#include <bits/stdc++.h>
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)>(b)?(b):(a))
using namespace std;
const int N=1e4+10;
int n,m,s,t,l=INT_MAX,r=INT_MIN,ans;
vector<pair<int,int>>g[N];
bool vis[N];
bool check(int x){
	memset(vis,0,sizeof vis);
	queue<int>q;
	q.push(s);
	vis[s]=1;
	while(!q.empty()){
		int temp=q.front();
		q.pop();
		for(auto&i:g[temp]){
			if(!vis[i.first]&&i.second<=x){
				if(i.first==t)return 1;
				vis[i.first]=1;
				q.push(i.first);
			}
		}
	}
	return vis[t];
}
int main(){
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		g[u].push_back({v,w});
		g[v].push_back({u,w});
		l=min(l,w);
		r=max(r,w);
	}
	while(l<=r){
		int mid=l+(r-l>>1);
		if(check(mid)){
			r=mid-1;
			ans=mid;
		}else{
			l=mid+1;
		}
	}
	cout<<ans;
	return 0;
}

……通讯加密中……

posted @ 2025-08-02 23:26  L-Coding  阅读(7)  评论(0)    收藏  举报