二分是什么?
咱们先思考一个问题,如果让你猜一个整数,每次猜会告诉你猜大了还是猜小了,范围是1~100,最坏情况下需要多少次呢?
没错,是100次,但是从1猜到100进度是不是非常的慢?这时候我们发现还有一个条件,每次猜完能知道大小,那能不能减少我们猜的次数呢?
很多人喜欢折中的解决办法,因为这样对两边都公平。这里我们也可以尝试一下折中。
第一次,我们猜50,猜小了,剩下的范围为51~100
第二次,我们猜75,猜小了,剩下的范围为76~100
第三次,我们猜88,猜小了,剩下的范围为89~100
第四次,我们猜94,猜小了,剩下的范围为95~100
第五次,我们猜98,猜小了,剩下的范围为99~100
第六次,我们猜99,猜小了,剩下的范围为100~100
可以看到,我们一直折中的猜,无论遇到的是大的情况还是小的情况,很少的次数就可以猜到结果,不信的同学可以尝试一下哦~
这种折中的方法就是二分!
二分能干啥?
通过刚刚的计算,我们明显感觉到,二分和暴力相比,计算量大幅下降,节约时间。
但是它也有限制条件,如果猜数字时不告诉我们是大是小,那么我们便无法使用二分方法,所以用二分解决的问题需要有顺序和范围,可以是大小、高低、远近等等。
我们能感觉到二分节约了时间,但是并不清晰,那我们扩大一下猜数字的范围再试试
比如1~10000
暴力枚举的时间复杂度是O(n) 也就是从1猜到n 猜10000次
通过二分方法,每次可以去掉当前一半的范围,那就是10000一直除2,log₂10000次 = 大概14次
一对比,我们的复杂度从O(n)级别降到O(logn)级别,有着巨大的优化啦
二分怎么实现?
第一步:取中值
在我们的思考中,折中就是取最大值和最小值中间的数,那该如何告诉计算机呢?
我们设左边界为 left = 1,右边界 right = 10000,中间值 mid = (left + right) / 2
这样计算便取到中间值了。
第二步:验证结果
取到中间值后,我们得去判断是猜大了还是猜小了,我们可以写一个 check(mid) 函数
对于我们猜测的 mid,我们可以把它代入原环境,看这个值是否符合我们的目标区间。
第三步:缩小范围
习惯上我们只分两个方向,即 包含答案的区间 和 不包含答案的区间。
对于猜数字,我们设 左边不包含猜中的情况,右边包含猜中的情况
如果猜小了,那么我们让 left = mid +1,舍去左半边,舍去边界。
猜大了或者猜中了 right = mid,舍去右半边,不舍去边界。
重复这三步,直到获取答案:
这样操作,我们便能得到新的一个新的区间范围,也满足有序条件,可以继续使用二分方法
最终我们会获得 left = right = mid 这样的情况,即是我们要的答案。
在此贴上c++的代码:
#include<bits/stdc++.h>
using namespace std;
int guess = rand()%10000+1;
bool check(int mid){
if(mid < guess) return false;
else if(mid >= guess) return true;
}
int main(){
int left = 1, right = 10000;
while(left < right){
int mid = (left + right) / 2;
if(check(mid)) right = mid;
else left = mid + 1;
}
cout<<left;
}
二分的其他使用方法
更新问题 猜数字=>猜数字区间:
区间1~10000,每次问数字会回复三种情况:猜大了,猜小了,在所猜区间中。
二分有很强的灵活性,假设让我们猜一个区间呢?是否也可以使用二分?
答案是可以的,我们可以灵活的改变 第二步中的 check条件 和 第三步中的变化条件。
比如先猜区间的左端点,猜小了为不满足的情况,猜大了和猜中区间为满足的情况,是否能使用二分操作呢? check 检查后,left 和 right 该如何变化呢?猜右端点又有什么变化呢?
这里留给大家思考,我在这里贴出我的一种处理方法:
#include<bits/stdc++.h>
using namespace std;
int guess_left = rand() % 10000 + 1;
int guess_right = guess_left + rand() % (10000 - guess_left);
bool check_left(int mid){ //判断左端点
if(mid < guess_left) return false;
else if(mid >= guess_left) return true;
}
bool check_right(int mid){ //判断右端点
if(mid > guess_right) return false;
else if(mid <= guess_right) return true;
}
int main(){
int ans_left, ans_right;
int left = 1, right = 10000;
//猜左端点
while(left < right){
int mid = (left + right) >> 1;
if(check_left(mid)) right = mid;
else left = mid + 1;
}
ans_left = left;
//猜右端点,初始化左右区间
left = 1, right = 10000;
while(left < right){
int mid = (left + right) >> 1;
if(check_right(mid)) left = mid;
else right = mid - 1;
}
ans_right = left;
cout<<"数字区间为:["<<ans_left<<","<<ans_right<<"]"<<endl;
return 0;
}