什么是二分?
我们首先来玩一个猜数游戏:我随便想一个1~100中的数字,
你的目标是以最少的次数猜到这个数字。
每次猜测后,我都会告诉你大了、小了或对了来验证结果。
有两种猜数方式:
第一种:假设你从1开始依次往上猜。
这种方式的风险极高,假如我想的数是1,那么只用猜一次;假如我想的数是99,则需要猜测99次才能猜对。
第二种:假设你从50开始折半猜。
1~100 猜50,小了。
50~100 猜75,大了。
50~75 猜62,大了。
50~62 猜56,对了!
这样每一次都取得当前可能区间的中间值,从而将余下的数排除一半。这种方法就叫做:二分法。
每一步的猜测都可以帮我们排除掉一半的错误结果,大大提高了查找答案的效率。
但是使用这个方法的首要前提是:需要查找的序列是有序排列的。
二分的实现
二分法最经典的写法应是如此:
在简单有序数组中查找指定元素
int binary_search(int low,int high,int key){
while(low <= high){
int mid = (low + high) / 2;
if(arr[mid] > key){
high = mid - 1;
}
else if(arr[mid] < key){
low = mid + 1;
}
else return mid;//找到元素,返回对应下标
}
return -1;//没有找到元素
}
下面我们基于一些例子,对二分的写法进行一定的优化和拓展。
1.洛谷 P2249 【深基13.例1】查找
https://www.luogu.com.cn/problem/P2249
题目描述:
输入 n 个不超过 10^9 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a1,a2,…,an,然后进行 m 次询问。对于每次询问,给出一个整数 q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 −1 。
首先看题目完全符合二分法的使用情景,但是与简单二分查找不一样的是,我们需要找到这个数字第一次出现时的位置。
AC代码如下:
#include <iostream>
using namespace std;
int m,n;
int que;
int arr[1000005];
int BSearch(int x){
int low = 1;
int high = n;
while(low < high){
int mid = low + (high-low) / 2;
if(x <= arr[mid]) high = mid;
else if(x > arr[mid]) low = mid + 1;
}
if(x == arr[low]){
return low;
}
else return -1;
}
int main(){
scanf("%d %d",&n,&m);
for(int i = 1;i <= n;i++){
scanf("%d",&arr[i]);
}
for(int k = 1;k <= m;k++){
scanf("%d",&que);
int ans = BSearch(que);
printf("%d ",ans);
}
return 0;
}
单独来看二分语句块:
int BSearch(int x){
int low = 1;
int high = n;
while(low < high){//循环终止条件变化
int mid = low + (high-low) / 2; //这样写可以避免爆出int范围
if(x <= arr[mid]) high = mid;//判断相等时并不直接输出,high的变化语句也有所改变
else if(x > arr[mid]) low = mid + 1;
}
if(x == arr[low]){
return low;
}
else return -1;
}
这样的写法也完全可以满足经典写法下的查找需求。
这里有几个值得思考的点,自己举出一些例子模拟一遍可以加深理解:
1.当循环终止时,low和high满足什么条件?
2.以下不同情形,最终返回值分别处于什么位置?
①查找值不存在,小于序列中所有数
②查找值不存在,大于序列中所有数
③查找值存在且唯一
④查找值存在但不唯一
⑤查找值不存在,但应在序列中部
3.最后的返回值是low,是否可以满足所有情形?
4.当low和high相邻时,二分的过程是怎么样的?
问题参考答案如下,可供参考:
1.low == high
2.①返回值指向第一个位置
②返回值指向最后一个位置
③返回值指向目标值位置
④返回值指向第一个目标值位置
⑤返回值总是low
3.4.当low和high相邻时,总是查询low指向的数,最终不管结果如何,low == high。最终总是low移动到(或指向)返回值的位置,所以可以满足所有情形。
二分答案
解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。当答案处于一定范围,且满足单调性,以枚举的方式很难一一列举时,把枚举换成二分,就变成了「二分答案」。
2.洛谷 P1873 [COCI 2011/2012 #5] EKO / 砍树
https://www.luogu.com.cn/problem/P1873
题目描述过长,大家可以自行跳转阅读
题目解读:我们可以在0到最高树高中枚举答案,但是从0枚举到最高树高太耗时间。我们可以在[0,max)的区间上进行二分作为答案,然后检查各个答案的可行性(一般使用贪心法)。这就是二分答案。
AC代码如下:
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
long long n;
long long m;
long long arr[1000005];
bool check(long long height){
long long sum = 0;
for(int i = 1;i <= n;i++){
if(arr[i] - height > 0){
sum += arr[i] - height;
}
}
return sum >= m;
}
int main(){
cin>>n>>m;
long long max = -1;
for(int i = 1;i <= n;i++){
scanf("%d",arr + i);
if(max <= arr[i]) max = arr[i];
}
long long low = 0;
long long high = max;
long long mid;
while(low + 1 < high){//二分范围[0,max)
mid = low + (high - low) / 2;
if(check(mid)) low = mid;
else high = mid;
}
cout<<low ;
return 0;
}
单独来看二分语句块:
long long low = 0;
long long high = max;
long long mid;
while(low + 1 < high){//二分范围[0,max)
mid = low + (high - low) / 2;
if(check(mid)) low = mid;
else high = mid;
}
cout<<low ;
同样提出几个值得思考的点:
1.与二分查找的语句之间有什么不同?
2.二分答案的范围应如何确定(视题目而定),这个很重要。
3.二分答案的范围区间为何是左闭右开?
4.为何返回值还是low?
5.思考如下图情况时,最终结果应是怎样?
给出结果图,其他问题没有标准答案,自己思考的结果就是问题的答案。
经过这一篇文章的学习,赶紧去做几个题练练手吧!
有疑问欢迎在评论区指出,相互探讨和学习!