二分详解

版权声明:八月炊火的博客如需转载请注明出处 https://blog.csdn.net/qq_34990731/article/details/83065667

引入:
小明和小强玩一个游戏,小强在纸上写下一个数(在1到1000之内),小明有十次询问的机会,每次询问小强只回答小明所说的数是大了还是小了或者等于,请问小明该怎么才能赢?
解思:
这个问题是二分的经典问题,由这个问题我们可以引出今天的主角——二分。
对于这个问题我们先说小明改怎么办,如果听不同也没关系,后面会详细解释。小明一开始要猜的范围在1到1000我们询问1和1000的平均值也就是500如果500大了我们就可以把范围缩小到1到499,小了的话同理可以缩小到501到1000,这样不断取答案存在的区间的最中间的数去询问不断缩小范围可以在log n的次数求出想要的答案。
思想:
二分法的思想是不断将待求解区间平均分成两份,根据求解区间中点的情况来确定答案所在的区间,这样就把解的范围缩小了一半。
详解:
二分这个东西真的很奇妙,可以把原本O(n)的时间复杂度降到O(log n),原理是这样的:在一个单调(说人话就是从大到小或从小到大)的序列中选择一个符合条件的数,上面就是一个经典的例子,这个二分其实不难,几句话就讲的明白,可难的是代码,有很多细节要注意否则可能样例过了可最后一个点都没过,这个细节后面再详细讲。这个主要难的是如何确定二分,我们有两种方法,一种是比较正常的是根据定义来,根据定义如果答案的区间满足有序性:单调递增或者单调递减,并且答案将整个区间分为两块,各是满足条件和不满足条件,具体条件是要看题目,如果满足这种性质就可以二分。还有一种方法比较玄学,是个人写题得出来经验,如果数据范围大于10^5 且小于10^9 这样如果时间复杂度是O(n^2)以及以上的话肯定过不了,然后O(n)的话不大现实如果你可以想出O(n)的做法话可以如果想不出来的话可以尝试一下二分,这样把时间复杂度降为O(log n*n),甚至更小,这样也比较容易。
代码:
这个是求一个单调递增区间(也就是从小到大)中比x大的数中最小的数。

while(l<r)
{
	int mid=(l+r)>>1;
	if(a[mid]>=x)//a数组为所给的数
		r=mid;
	else l=mid+1;
}
return a[l];

这个是求一个单调递增区间中比x小的数中最大的数。

while(l<r)
{
	int mid=(l+r+1)>>1;
	if(a[mid]<=x)//a数组为所给的数
		l=mid;
	else r=mid-1;
}
return a[l];

这两个是二分的模板,可以发现这两个区别是微小的,也就是mid的计算要不要+1,这里总结一下。我们二分主要是确定mid是否符合条件,无论符不符合条件我们都可以缩小到原来的一半,现在关键的就是符合条件应该怎么修改缩小后的区间。这边有两个区别就是到底是左端区间往mid缩小还是右端点往mid缩小。
(以下都是讨论符合条件的情况下)如果是左端点往mid缩小的话则计算mid时+1就像第二个代码,如果是右端点往mid计算的话就不用+1,就像第一个代码。是不是晕了,我也觉得,所以给一个超强代码:

while(l<=r)
{
	int mid=(l+r)>>1;
	if(check(mid))
		l=mid+1;
	else r=mid-1;
}

这个区别就是while的判定条件加一个等于然后r的修改是mid-1,l的修改是mid+1这个适用于所有题目。

实数型二分:
这个又是什么东东,其实上面讲的二分严格来说是整数型二分,也就是说整个区间是整数,而这个顾明思议整个区间是实数,也就是既有小数又有整数,因为这个比较复杂,所以如果可以确定是整数的话就还是用上面的,遇到小数才用这个。
代码:

double l = 0, r = n;
while(r-l<=res)//这里的res是一个精度的判断,通常题目都是要求你求一个满足条件的小数,然后误差可以在一个小数之内(例如在0.01之内),这个小数就是res
{
	double mid = (l + r) / 2;
	if (check(mid))
		l = mid;
	else r = mid;
 }

这个实数型的二分不经常用,如果用的话还要注意double计算会掉精度所以还要加一个掉精度的范围(讲人话就是double在计算时如果小数位太多会自动没掉一些,所以要加一个范围判断,即在check中最后答案比较是在0.000001左右都可以,0.000001是亲测的范围,适用于广大题目,不过不是所有,有能力的最好自己根据题目确定)。

猜你喜欢

转载自blog.csdn.net/qq_34990731/article/details/83065667
今日推荐