二分写法总结

文章目录

  • 写在前面
  • 整数二分
    • 二分查找
  • 二分答案
    • 二分求上界
    • 二分求下界
  • 浮点数二分

写在前面

首先二分的思想不难,问题在于整数二分的时候如果没有处理好二分的区间,会导致死循环的情况,比如下面这种二分求上界的写法

int binarySearch(int l, int r)
{
	while(l < r)
	{
		int mid = (l + r) / 2;
		if(check(mid)) l = mid;
		else r = mid;
	}
	return l;
}

这样写到最后二分的区间是[l, l+1),且如果check(l)返回的是true,那么将会陷入死循环,因为这时候mid始终等于l。
其次二分应用的情况很多,对于每种情况,写法也会不同,所以在这里总结下对于各种情况二分的写法

整数二分

二分查找

二分查找也有很多情况,例如在有序数组中找某一个数(记为a)的位置,如果只有一个数,那直接返回这个数的位置就好了,但是如果存在好几个相同的该数字,二分又可以分为找第一个出现的位置(即数组中第一个大于等于a的数的位置)或者最后一个出现的位置(即第一个大于a的数的位置的前一位),这两种就是所谓的下界和上界,这两种我们放到二分答案的情况里说明

这里我们先看每种数只有一个的情况,假设a数组元素严格递增

int binarySearch(int *a, int l, int r, int key)//key是我们要找的数
{
    while(l < r)
    {
        int mid = (l + r) / 2;
        if(a[mid] < key) l = mid + 1;
        else if(a[mid] == key) return mid;
        else r = mid;
    }
    return -1;
}

这就是最简单的二分了,我们来具体看看它的计算过程

首先该写法它的 l 和 r,即我们二分的区间的左右端点,它是保证要找的数在[l, r)里的,也就是区间左端点可能是我们要找的数,而区间右端点不可能是

当区间中点mid偏小时,l = mid + 1,二分区间变成[mid + 1, r),如果该数字存在,必定还在该区间内;

如果mid偏大,r = mid,mid不可能是我们要找的数,而r端点本来就不会是答案,所以可以这么写

所以每次二分区间[l, r),区间必定会缩小,不可能死循环,最后一次判定时(如果一直没找到该数),区间变成[l, l + 1),这时候mid = l,如果mid还不等于我们要找的数,区间就会缩小到 l == r ,便退出了循环,返回-1表示没找到

虽然该写法没什么大问题,而且比较简洁,但是我们需要注意:

  • mid = (l + r) / 2 ,l + r可能会超过int范围,我们也可以写成 l + (r - l) / 2,可以了防止溢出,并且具有更好的通用性(如果 l 和 r 是迭代器或者指针,第一种就不行了, 因为迭代器或指针是不能相加的)(引用自litmxs博主的二分写法)

二分答案

二分答案是信息学竞赛中一种常用的技巧,首先我们要搞明白什么情况下可以二分答案

当我们的答案具有连续性,单调性的时候,我们就可以二分答案,那什么叫答案具有连续性呢?即可能的答案区间 [l, r] 中,x假如可能是答案,那么x - 1,x + 1可能也是答案;

什么叫单调性呢?即当x越来越大,就会越来越难以/容易满足题目要求

二分答案的过程往往会涉及到求答案的上、下界

所谓上下界,即假设我们二分的潜在答案区间为[l, r], 假设可行的答案区间为[L, R],那么L就是下界,R就是上界,即答案区间的左右端点,往往分别对应着 最少/小 和 最多/大

二分求上界

当我们设置二分潜在的答案区间为左闭右开的时候,即 [l, r),最终得到的 l 就是答案的上界,因为这时候 l 的右侧答案都已经不符合要求,l必定是答案中最大的了

int upperBound(int l, int r)
{
    while(l + 1 < r) //循环条件为区间长度>=2
    {
        int mid = (l + r) / 2;
        if(check(mid)) l = mid; //检查mid是否符合题目要求
        else r = mid;
    }
    return l;
}

循环结束后区间大小就是1,即 [l, l+1),最终答案就是l
但是这样的写法需要注意以下3点:

  1. 初始二分的潜在的答案区间的右端点r要设置成比最大可能答案大,因为r不会被访问到
  2. 还是 l + r 可能溢出
  3. 如果题目说了可能没有答案,最后需要检查下 l,因为可能二分的区间里没有答案,每次循环都是 r = mid,最终的l没有被检查过

二分求下界

当我们设置二分潜在的答案区间为左开右闭的时候,即 (l, r],最终得到的 r 就是答案的下界,因为这时候 r 的左侧答案都已经不符合要求,r必定是答案中最小的了

int lowerBound(int l, int r)
{
    while(l + 1 < r) //循环条件为区间长度>=2
    {
        int mid = (l + r) / 2;
        if(check(mid)) r = mid; //检查mid是否符合题目要求
        else l = mid;
    }
    return r;
}

同样的我们也需要注意以下三点:

  1. 初始二分的潜在的答案区间的左端点l要设置成比最小可能答案大小,因为l不会被访问到
  2. 还是 l + r 可能溢出
  3. 如果题目说了可能没有答案,最后需要检查下 r,因为可能二分的区间里没有答案,每次循环都是 l = mid,最终的r没有被检查过

浮点数二分

浮点数二分基本不会有什么问题,因为不会有整数二分取整没取好导致死循环的问题

有两种写法:

  1. 以循环次数为循环终止条件

    for(int i = 0; i < 60; ++i)//循环60次就已经可以达到很高的精度了
    {
        mid = (l + r) / 2;
        //检查mid
    }
    
  2. 以精度位循环终止条件

    while(r - l < eps)//eps为题目要求的精度
    {
        mid = (l + r) / 2;
        //检查mid
    }
    

比较推荐第一种写法,因为第二种如果精度设置过小,加上浮点数的精度问题还是可能死循环

你可能感兴趣的:(二分)