今天继续刷完二分查找,还有最后五个题二分查找就结束啦!
- 023、 P3743 小鸟的设备
题目背景
小鸟有 n n n 个可同时使用的设备。
题目描述
第 i i i 个设备每秒消耗 a i a_i ai 个单位能量。能量的使用是连续的,也就是说能量不是某时刻突然消耗的,而是匀速消耗。也就是说,对于任意实数,在 k k k 秒内消耗的能量均为 k × a i k\times a_i k×ai 单位。在开始的时候第 i i i 个设备里存储着 b i b_i bi 个单位能量。
同时小鸟又有一个可以给任意一个设备充电的充电宝,每秒可以给接通的设备充能 p p p 个单位,充能也是连续的,不再赘述。你可以在任意时间给任意一个设备充能,从一个设备切换到另一个设备的时间忽略不计。
小鸟想把这些设备一起使用,直到其中有设备能量降为 0 0 0。所以小鸟想知道,在充电器的作用下,她最多能将这些设备一起使用多久。
输入格式
第一行给出两个整数 n , p n,p n,p。
接下来 n n n 行,每行表示一个设备,给出两个整数,分别是这个设备的 a i a_i ai 和 b i b_i bi。
输出格式
如果小鸟可以无限使用这些设备,输出 − 1 -1 −1。
否则输出小鸟在其中一个设备能量降为 0 0 0 之前最多能使用多久。
设你的答案为
a
a
a,标准答案为
b
b
b,只有当
a
,
b
a,b
a,b 满足
∣
a
−
b
∣
max
(
1
,
b
)
≤
1
0
−
4
\dfrac{|a-b|}{\max(1,b)} \leq 10^{-4}
max(1,b)∣a−b∣≤10−4 的时候,你能得到本测试点的满分。
样例 #1
样例输入 #1
2 1
2 2
2 1000
样例输出 #1
2.0000000000
样例 #2
样例输入 #2
1 100
1 1
样例输出 #2
-1
样例 #3
样例输入 #3
3 5
4 3
5 2
6 1
样例输出 #3
0.5000000000
提示
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 100000 1\leq n\leq 100000 1≤n≤100000, 1 ≤ p ≤ 100000 1\leq p\leq 100000 1≤p≤100000, 1 ≤ a i , b i ≤ 100000 1\leq a_i,b_i\leq100000 1≤ai,bi≤100000。
解题思路
对于这道题思路肯定还是二分,我们发现这个二分的答案是小数,我们可以先二分出符合条件的最大整数x,然后再在x到x+1的范围内按照0.00001的精度去二分出我们想要的答案。
下一步就是check函数如何写的问题。给定我们一个时间,我们去判断设备能不能撑到这个时间。
思路就是,遍历每一个设备,如果设备本身的电量够用,则忽略,如果不够,则计算需要多少充电宝的电量,累加需要的充电宝的电量,最后如果不超过充电宝的电量,则可以撑到这个时间。
注意
1. 范围问题,一旦有溢出的可能,都要开longlong
2.答案的范围,先确定好答案的范围再去二分!!!
代码
#include<iostream>
using namespace std;
const int N = 1e5 +10;
int n,p;
int a[N],b[N];
long long suma;
long long sumb;
bool check1(long long mid){
long long pro=p*mid;
for(int i=1;i<=n;i++){
long long temp=a[i]*mid-b[i];
if(temp<0)temp=0;
pro-=temp;
if(pro<0)return false;
}
return true;
}
bool check2(double mid){
double pro=p*mid;
for(int i=1;i<=n;i++){
double temp=a[i]*mid-b[i]*1.0;
if(temp<0.0)temp=0.0;
pro-=temp;
if(pro<0.0)return false;
}
return true;
}
//精度问题,但凡涉及超出范围的都要使用long long
//边界问题,一定要确定好的二分答案的边界
int main(){
cin>>n>>p;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
suma+=a[i];
sumb+=b[i];
}
if(suma<=p){
cout<<-1;
return 0;
}
long long l=0,r=1e10;
while(l<r){
long long mid=l+r+1>>1;
if(check1(mid)){
l=mid;
}else r=mid-1;
}
double ld=(double)l,rd=ld+1.0;
while(rd-ld>0.000001){
double midd=(ld+rd)/2;
if(check2(midd)){
ld=midd;
}else{
rd=midd;
}
}
cout<<ld;
}
题目描述
对于给定的一个长度为 N N N 的正整数数列 A 1 ∼ N A_{1\sim N} A1∼N,现要将其分成 M M M( M ≤ N M\leq N M≤N)段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 4 2 4 5 1 4\ 2\ 4\ 5\ 1 4 2 4 5 1 要分成 3 3 3 段。
将其如下分段:
[ 4 2 ] [ 4 5 ] [ 1 ] [4\ 2][4\ 5][1] [4 2][4 5][1]
第一段和为 6 6 6,第 2 2 2 段和为 9 9 9,第 3 3 3 段和为 1 1 1,和最大值为 9 9 9。
将其如下分段:
[ 4 ] [ 2 4 ] [ 5 1 ] [4][2\ 4][5\ 1] [4][2 4][5 1]
第一段和为 4 4 4,第 2 2 2 段和为 6 6 6,第 3 3 3 段和为 6 6 6,和最大值为 6 6 6。
并且无论如何分段,最大值不会小于 6 6 6。
所以可以得到要将数列 4 2 4 5 1 4\ 2\ 4\ 5\ 1 4 2 4 5 1 要分成 3 3 3 段,每段和的最大值最小为 6 6 6。
输入格式
第 1 1 1 行包含两个正整数 N , M N,M N,M。
第 2 2 2 行包含 N N N 个空格隔开的非负整数 A i A_i Ai,含义如题目所述。
输出格式
一个正整数,即每段和最大值最小为多少。
样例 #1
样例输入 #1
5 3
4 2 4 5 1
样例输出 #1
6
提示
对于 20 % 20\% 20% 的数据, N ≤ 10 N\leq 10 N≤10。
对于 40 % 40\% 40% 的数据, N ≤ 1000 N\leq 1000 N≤1000。
对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 5 1\leq N\leq 10^5 1≤N≤105, M ≤ N M\leq N M≤N, A i < 1 0 8 A_i < 10^8 Ai<108, 答案不超过 1 0 9 10^9 109。
解题思路
和的最大值最小,二分右区间的最小值。核心依然是check函数怎么写。
注意
一定要合理的考虑答案的范围,最小的答案应该是数列中元素的最大值,最大的答案应该是数列的和
代码
#include<iostream>
using namespace std;
const int N = 1e5 +10;
int n,m;
int a[N];
long long pre[N];
int max_a;
bool check(long long mid){
int sum=0;
int cnt=0;
for(int i=0;i<=n;i++){
if(sum+a[i]<=mid){
sum+=a[i];
}
else {
sum=a[i];
cnt++;
if(cnt>m-1)return false;
}
}
return true;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
max_a=max(max_a,a[i]);
}
//一定要合理的考虑答案的范围,最小的答案应该是数列的最大值,最大的答案应该是数列的和
int l=max_a,r=1e8;
while(l<r){
long long mid = l+r>>1;
if(check(mid))r=mid;
else l=mid+1;
}
cout<<l;
}
- 025、P2440 木材加工
题目背景
要保护环境
题目描述
木材厂有 n n n 根原木,现在想把这些木头切割成 k k k 段长度均为 l l l 的小段木头(木头有可能有剩余)。
当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。
木头长度的单位是 cm \text{cm} cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为 11 11 11 和 21 21 21,要求切割成等长的 6 6 6 段,很明显能切割出来的小段木头长度最长为 5 5 5。
输入格式
第一行是两个正整数 n , k n,k n,k,分别表示原木的数量,需要得到的小段的数量。
接下来 n n n 行,每行一个正整数 L i L_i Li,表示一根原木的长度。
输出格式
仅一行,即 l l l 的最大值。
如果连
1cm
\text{1cm}
1cm 长的小段都切不出来,输出 0
。
样例 #1
样例输入 #1
3 7
232
124
456
样例输出 #1
114
提示
数据规模与约定
对于 100 % 100\% 100% 的数据,有 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1≤n≤105, 1 ≤ k ≤ 1 0 8 1\le k\le 10^8 1≤k≤108, 1 ≤ L i ≤ 1 0 8 ( i ∈ [ 1 , n ] ) 1\le L_i\le 10^8(i\in[1,n]) 1≤Li≤108(i∈[1,n])。
思路
常规题,比较简单,关键也是在于check函数,给定一个答案,判断这个答案是否可行
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 +10;
int n,k;
int a[N];
bool check(int mid){
int num=0;
for(int i=0;i<n;i++){
num+=a[i]/mid;
if(num>=k)return true;
}
return false;
}
int main(){
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
int l=0,r=1e8;
while(l<r){
int mid = l+r+1>>1;
if(check(mid))l=mid;
else r=mid-1;
}
cout<<l;
}
题目描述
一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N N N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M M M 块岩石(不能移走起点和终点的岩石)。
输入格式
第一行包含三个整数 L , N , M L,N,M L,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L ≥ 1 L \geq 1 L≥1 且 N ≥ M ≥ 0 N \geq M \geq 0 N≥M≥0。
接下来 N N N 行,每行一个整数,第 i i i 行的整数 D i ( 0 < D i < L ) D_i\,( 0 < D_i < L) Di(0<Di<L), 表示第 i i i 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。
输出格式
一个整数,即最短跳跃距离的最大值。
样例 #1
样例输入 #1
25 5 2
2
11
14
17
21
样例输出 #1
4
提示
输入输出样例 1 说明
将与起点距离为 2 2 2 和 14 14 14 的两个岩石移走后,最短的跳跃距离为 4 4 4(从与起点距离 17 17 17 的岩石跳到距离 21 21 21 的岩石,或者从距离 21 21 21 的岩石跳到终点)。
数据规模与约定
对于
20
%
20\%
20%的数据,
0
≤
M
≤
N
≤
10
0 \le M \le N \le 10
0≤M≤N≤10。
对于
50
%
50\%
50% 的数据,
0
≤
M
≤
N
≤
100
0 \le M \le N \le 100
0≤M≤N≤100。
对于
100
%
100\%
100% 的数据,
0
≤
M
≤
N
≤
50000
,
1
≤
L
≤
1
0
9
0 \le M \le N \le 50000,1 \le L \le 10^9
0≤M≤N≤50000,1≤L≤109。
解题思路
最短距离的最大值,仍然是二分。给定一个答案ans,判断答案是否可行。
check函数思路,遍历石头坐标,计算和上一个的距离,如果比ans小,那就移走这块石头(代码上只需要将其位置和上一块石头位置相等即可),同时cnt++,当cnt大于最多可以移走的数目,则不可行。
不要忘记判断起点和终点的石头
对于check函数操作原始数据时,我们最好是开辟一份拷贝数组,不要动原始数据
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N =5e4 +10;
int q,n,m;
int d[N];
int a[N];
bool check(int mid){
int cnt=0;
//一定要拷贝一份数组进行操作,不然后面数据都出错了
memcpy(a,d,sizeof(d));
for(int i=1;i<=n;i++){
if(a[i]-a[i-1]<mid){
//把这个石头移走
a[i]=a[i-1];
cnt++;
//cout<<i<<"移走\n";
if(cnt>m){
//cout<<"不可\n";
return false;
}
}
}
//需要注意的是,最后一块石头到终点也是需要考虑的呢
if(a[n+1]-a[n]<mid){
cnt++;
if(cnt>m)return false;
}
return true;
}
int main(){
cin>>q>>n>>m;
for(int i=1;i<=n;i++)cin>>d[i];
d[n+1]=q;
int l=1,r=q;
while(l<r){
int mid =l+r+1>>1;
if(check(mid)){
l=mid;
}else r=mid-1;
//cout<<mid<<"\n";
}
//check(7);
cout<<l;
}
题目背景
B 市和 T 市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。
题目描述
现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。
输入格式
第 1 1 1 行包括三个数 L , N , K L,N,K L,N,K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。
第 2 2 2 行包括递增排列的 N N N 个整数,分别表示原有的 N N N 个路标的位置。路标的位置用距起点的距离表示,且一定位于区间 [ 0 , L ] [0,L] [0,L] 内。
输出格式
输出 1 1 1 行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。
样例 #1
样例输入 #1
101 2 1
0 101
样例输出 #1
51
提示
公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点 50 50 50 或 51 51 51 个单位距离处,这样能达到最小的空旷指数 51 51 51。
50 % 50\% 50% 的数据中, 2 ≤ N ≤ 100 2 \leq N \leq 100 2≤N≤100, 0 ≤ K ≤ 100 0 \leq K \leq 100 0≤K≤100。
100 % 100\% 100% 的数据中, 2 ≤ N ≤ 100000 2 \leq N \leq 100000 2≤N≤100000, 0 ≤ K ≤ 100000 0 \leq K \leq100000 0≤K≤100000。
100 % 100\% 100% 的数据中, 0 < L ≤ 10000000 0 < L \leq 10000000 0<L≤10000000。
解题思路
这个题是最大距离的最小值,和上一个题还是蛮像的。整体是二分,核心仍然是check函数怎么写。
我们遍历数组,计算距离,如果距离大于mid,此时需要放路障,再当前路障上和上一个路障之间放置temp=(a[i]-a[i-1])/mid个(注意特殊情况,temp%mid==0时放temp-1个),如果最后放置路障的个数小于m,则可行。
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N =1e5 +10;
int q,n,m;
int d[N];
int a[N];
bool check(int mid){
int cnt=0;
//一定要拷贝一份数组进行操作,不然后面数据都出错了
//当然对这个题不需要,但是养成好习惯
memcpy(a,d,sizeof(d));
for(int i=1;i<n;i++){
if(a[i]-a[i-1]>mid){
//在中间加若干个路障以满足条件
int temp=(a[i]-a[i-1])/mid;
if((a[i]-a[i-1])%mid==0){
cnt+=temp-1;
}
else cnt+=temp;
//cout<<i<<"移走\n";
if(cnt>m){
//cout<<"不可\n";
return false;
}
}
}
return true;
}
int main(){
cin>>q>>n>>m;
for(int i=0;i<n;i++){
cin>>d[i];
}
int l=1,r=q;
while(l<r){
int mid =l+r>>1;
if(check(mid)){
r=mid;
}else l=mid+1;
//cout<<mid<<"\n";
}
//check(7);
cout<<l;
}
二分刷题总结
总的来看,二分分为二分查找和二分答案,前者题目比较容易判断,对于后者而言,需要我们自己灵活判断,一般题目具有这样的特点。
- 让你找某个量的最大值或者最小值
- 题目正向思考答案不好想,但是如果给定一个答案可以判断它是否成立
这时可以考虑用二分来做
结语
行动是解决焦虑的最好办法!