一、算法思想:
二分答案 与二分查找类似,二分查找有一个前提就是数列要求是有序的,二分答案则是要求 满足条件的答案是单调有序的,它的基本思想是在答案可能的范围 ([L,R]) 内二分查找答案,不断检查当前答案是否满足题目的要求,根据检查结果 更新查找的区间,最终取得最符合题目要求的答案进行输出。
答案的单调性:
如何理解答案的单调性呢?可以见下图,该图表示一个区间,从左到右要实现的代价逐渐增高,我们要在这个区间里找到一个代价最小的满足条件的答案。观察图可以发现,位于答案左边的区间不满足条件,位于答案右边的区间满足条件,像这种要么一边不满足,要么一边都满足的现象就是答案的单调性。
二、代码实现:
1.例题:
接下来我们通过一道题目(P1873 [COCI 2011/2012 #5] EKO / 砍树)来具体的了解一下二分答案。
【题目描述】:
伐木工人 Mirko 需要砍 M 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。
Mirko的伐木机工作流程如下:Mirko 设置一个高度参数 H(米),伐木机升起一个巨大的锯片到高度 H,并锯掉所有树比 H 高的部分(当然,树木不高于 H 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 20,15,10 和17,Mirko 把锯片升到 15 米的高度,切割后树木剩下的高度将是 15,15,10 和 15,而 Mirko 将从第 1 棵树得到 5米,从第 4 棵树得到 2 米,共得到 7 米木材。
Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 H,使得他能得到的木材至少为 M 米。换句话说,如果再升高 1 米,他将得不到 M 米木材。
【输入格式】:
第 1 行 2 个整数 N 和 M,N 表示树木的数量,M 表示需要的木材总长度。
第 2 行 N 个整数表示每棵树的高度。
【输出格式】:
1 个整数,表示锯片的最高高度。【输入样例#1】:
4 7
20 15 10 17
【输出样例#1】:
15【输入样例#2】:
5 20
4 42 40 26 46
【输出样例#2】:
36【说明/提示】:
对于 100% 的测试数据,1≤N≤ 1 0 6 10^{6} 106, 1≤M≤2× 1 0 9 10^{9} 109,树的高度 < 1 0 9 10^{9} 109,所有树的高度总和 >M。
这道题目本质上就是要求一个最大高度,使得砍到的树的总长度满足条件,拿样例 1 举例,使用图片展示出来就是这样的:
假设 ans 是满足条件的最大高度,那么在图片中展示就是这样的:
大于 ans 的高度,不满足条件,小于 ans 的高度,都满足条件,这样的答案具有 单调性,所以考虑使用 二分答案 。
2.算法思路:
二分答案的实现思路,大致上可以分为三个部分:
1.确定答案所在的区间
2.判断 mid 是否满足条件,更新答案区间
3.得到符合题目要求的答案
我们根据思路,一步一步来实现:
① 首先是第一步,确定答案所在的区间。我们要求一个满足条件的最大高度,那么答案的范围就应该是从 0 到树的最大高度 height,也就是 [0,height]。
② 区间确定完了,接下来就可以使用二分查找的框架去判断 mid 是否满足条件,然后根据判断结果去更新答案的区间,如果计算得到的总长度 sum 大于等于 m 则说明高度低了,将 left 的值设为 mid+1,否则将 right 的值设为 mid-1。
int left=0,right=height,mid,ans;
while(left<=right){//二分框架
mid=(left+right)/2;
long long sum=0; //累加会超过int范围
//计算高度为mid时,能锯的木材总量
for(int i=1;i<=n;i++){
if(a[i]>mid) sum+=a[i]-mid;
}
//判断是否达到要求的木材总量m,更新区间
if(sum>=m) ans=mid,left=mid+1;
else right=mid-1;
}
③ 最后一步就是将结果输出
printf("%d",ans);
最后我们将代码整合一下,用于求解这道题目的代码就是这样的。
3.C++代码实现:
#include<bits/stdc++.h>
using namespace std;
int n,m,a[1000005],height;
int Binary_answer(int h){
int left=0,right=h,mid,ans;
while(left<=right){ //二分框架
mid=(left+right)/2;
long long sum=0; //累加会超过int范围
//计算高度为mid时,能锯的木材总量
for(int i=1;i<=n;i++){
if(a[i]>mid) sum+=a[i]-mid;
}
//判断是否达到要求的木材总量m,更新区间
if(sum>=m) ans=mid,left=mid+1;
else right=mid-1;
}
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i]>height) height=a[i];
}
cout<<Binary_answer(height);
return 0;
}
三、二分答案分类
二分答案根据所求的问题可以大体上分为以下三个类别:
①求满足条件的最大(小)值
②最大值最小化问题
③最小值最大化问题
下面我们分别通过三道例题来认识一下这三个问题。
1.求满足条件的最大(小)值
这类问题,其实就是最常见的二分答案,上面的那道 砍树 就属于这一类。接下来再来看一道新题 (P2440 木材加工) ,可以试着先做一下。
【题目描述】:
木材厂有 n 根原木,现在想把这些木头切割成 k 段长度均为 l l l 的小段木头(木头有可能有剩余)。
当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。
木头长度的单位是 cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为 11 和 21,要求切割成等长的 6 段,很明显能切割出来的小段木头长度最长为 5。
【输入格式】:
第一行是两个正整数 n,k,分别表示原木的数量,需要得到的小段的数量。
接下来 n 行,每行一个正整数 L i L_{i} Li ,表示一根原木的长度。
【输出格式】:
仅一行,即 l l l 的最大值。
如果连 1cm 长的小段都切不出来,输出 0。
【输入样例#1】:
3 7
232
124
456【输出样例#1】:
114【说明/提示】:
对于 100% 的数据,有 1≤ n ≤ 1 0 5 10^{5} 105,1≤ k ≤ 1 0 8 10^{8} 108,1≤ L i L_i Li ≤ 1 0 8 10^{8} 108 ( i i i∈[1,n])。
这道题思路上没有太多的变化,只是增加了一些细节,完整代码如下。
C++代码实现:
#include <bits/stdc++.h>
using namespace std;
int n,m,a[100005],height;
long long sum;
int Binary_answer(int h){
int left=0,right=h,mid,ans;
while(left<=right){
mid=(left+right)/2;
int sum1=0;
for(int i=1;i<=n;i++){
sum1+=a[i]/mid;//计算长度为mid时能切割的段数
}
//判断大小,更新区间
if(sum1>=m) ans=mid,left=mid+1;
else right=mid-1;
}
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];//求出木材的总和
height=max(height,a[i]); //找到所有木材中的最大值
}
if(sum<m){//如果总和小于m,说明连长度切1厘米都无法切m段
cout<<0;
return 0;
}
//二分答案
cout<<Binary_answer(height);
return 0;
}
2.最大值最小化问题
第二类问题是最大值最小化问题,这种问题,往往会求一个最小的最大值,相当于在第一类问题上增加了一层限制,可以直接看例题:(P1182 数列分段 Section II)
【题目描述】:
对于给定的一个长度为N的正整数数列 A 1 ∼ N A _{1∼N} A1∼N ,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 4 2 4 5 1 要分成 3 段。
将其如下分段: [4 2][4 5][1]
第一段和为 6,第 2 段和为 9,第 3 段和为 1,和最大值为 9。
将其如下分段: [4][2 4][5 1]
第一段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。
并且无论如何分段,最大值不会小于 6。
所以可以得到要将数列 4 2 4 5 1 要分成 3 段,每段和的最大值最小为 6。
【输入格式】:
第 1 行包含两个正整数 N,M。
第 2 行包含 N 个空格隔开的非负整数 A i A_i Ai ,含义如题目所述。
【输出格式】:
一个正整数,即每段和最大值最小为多少。
【输入样例】:
5 3
4 2 4 5 1
【输出样例】:
6
【说明/提示】:
对于 20% 的数据,N≤10。
对于 40% 的数据,N≤1000。
对于 100% 的数据,1≤N≤ 1 0 5 10^5 105 ,M≤N,$A_i $< 1 0 8 10^8 108, 答案不超过 1 0 9 10^9 109。
实现思路:
1.确定最大值所在的区间,left 和 right
2.二分判断最大值为 mid 时数列是否可以分成 m 段
3.根据判断结果更新区间
4.得到符合题目要求的答案
接下来按照思路进行分析:
①完成第一步,确定区间。要求最大值最小,则该值必定是大于数列中的最大值的(最大值单独为一段的时候),最大值则为所有数列之和。所以最大值的区间 left=数列中的最大值,right=数列的所有数相加之和,则:
int n,m,a[100005];
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];//sum为答案右边界
mx=max(mx,a[i]); //mx为答案左边界
}
②二分判断最大值为 mid 时,数列是否可以分成 m 段,采用贪心策略,分割的每一段的总和尽量接近但不超过 mid,则:
while(left<=right){
mid=(left+right)/2;
//计算最大值为mid时,能分割的段数
int sum1=0,cnt=1;//分别表示当前这一段的数字总和、数列目前段数
for(int i=1;i<=n;i++){
//判断a[i]是否可以连接到当前这段数字中
if(sum1+a[i]<=mid) sum1+=a[i];//sum+a[i]不超过mid则可以继续连接
else sum1=a[i],cnt++;//否则a[i]自起一段,数列总段数cnt+1
}
}
③根据判断结果更新区间,因为是求最大值的最小,所以区间上是向左找的。
当 cnt<=m 时,说明 mid 值偏大,此时去查看更小的,即 ans=mid,right=mid-1,如果分割的段数超过 m 段,说明 mid 要更大一点,即 left=mid+1。
//判断结果,更新区间
if(cnt<=m) ans=mid,right=mid-1;
else left=mid+1;
④得到符合题目要求的答案:
printf("%d",ans);
完整代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,a[100005];
int sum,mx;
int Binary_answer(int l,int r){
int left=l,right=r,mid,ans;
while(left<=right){
mid=(left+right)/2;
//计算最大值为mid时,能分割的段数
int sum1=0,cnt=1; //分别表示当前这一段的数字总和、数列目前段数
for(int i=1;i<=n;i++){
//判断a[i]是否可以连接到当前这段数字中
if(sum1+a[i]<=mid) sum1+=a[i]; //sum+a[i]不超过mid则可以继续连接
else sum1=a[i],cnt++; //否则a[i]自起一段,数列总段数cnt+1
}
//判断结果,更新区间
if(cnt<=m) ans=mid,right=mid-1;
else left=mid+1;
}
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i]; //sum为答案右边界
mx=max(mx,a[i]); //mx为答案左边界
}
//二分答案
cout<<Binary_answer(mx,sum);
return 0;
}
3.最小值最大化问题
第三类是最小值最大化问题,思路和第二类问题相似,要求最大的最小值,就比如下面这道题(P1824 进击的奶牛),可以先尝试做做看。
【题目描述】:
Farmer John 建造了一个有 N(2≤ N ≤100000)个隔间的牛棚,这些隔间分布在一条直线上,坐标是 x 1 , . . . , x N ( 0 ≤ x i ≤ 1000000000 ) x_1,...,x_N(0 ≤ x_i≤1000000000) x1,...,xN(0≤xi≤1000000000)。
他的 C(2≤ C≤ N)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John 想把这些牛安置在指定的隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢?【输入格式】:
第 1 行:两个用空格隔开的数字 N 和 C。
第 2 ~ N+1 行:每行一个整数,表示每个隔间的坐标。【输出格式】:
输出只有一行,即相邻两头牛最大的最近距离。【输入样例】:
5 3
1
2
8
4
9【输出样例】:
3
实现思路:
1.确定最大值所在的区间,left 和 right
2.二分判断最小值为 mid 时数列是否可以安置 m 头奶牛
3.根据判断结果更新区间
4.得到符合题目要求的答案
思路和第二类问题类似,这里就不详细更新步骤了,直接看完整代码
代码实现:
#include <bits/stdc++.h>
using namespace std;
int n,m,a[100005];
int mn,mx;
int Binary_answer(int l,int r){
int left=l,right=r,mid,ans;
while(left<=right){
mid=(left+right)/2;
//计算最小值为mid时可以放置的奶牛数量
int index=1,cnt=1;//index为当前坐标,cnt为奶牛数量
for(int i=2;i<=n;i++){
//判断a[i]和当前坐标之前的差值
if(a[i]-a[index]>=mid) index=i,cnt++;//条件成立,更新坐标,奶牛数量+1
}
//判断结果,更新区间
if(cnt>=m) ans=mid,left=mid+1;
else right=mid-1;
}
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
//将坐标从小到大排序,方便计算
sort(a+1,a+n+1);
//确定区间最大值和最小值
mn=1; //默认最小值为1,就是紧挨着
mx=a[n]-a[1]; //最大值为最后一个坐标减第一个坐标
//二分答案
cout<<Binary_answer(mn,mx);
return 0;
}