二分习题补充
L-Coding于2025.8.2——2025.8.3作。
一、浮点二分
※ 注意事项
- 维护
double
类型指针; - 修改指针时不应加减
1
; while
循环条件应为r-l>delta
,其中delta
为误差,一般设置为形如1e-4
或1e-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
内保持工作需要充电器额外供给的电能)。
再考虑二分写法。左右边界可以设为0
和1e10
(数据上限),通过题中给出的允许误差Δ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 银行贷款
我数学不好。
简单来说,这道题就是求式
中\(ans\)的值。
考虑模拟原求和式的计算过程,模拟法实现即可,不多赘述。
注意到题中表明\(ans\)存在一个范围,如果\(ans\)单调,那么就可以用二分答案得出答案。不妨分析答案单调性。当\(ans\)变大时,分式分母变大,分式变小,原式变小,故原求和式单调递减。
于是我们的check(x)
函数同样返回布尔类型,但返回值却反映原求和式的值sum
与w0
的大小关系(大于或小于),即返回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;
}
……通讯加密中……