……

二分法的边界处理

0 前言

  本文分享了二分法的原理及左闭右闭和左闭右开两种不同写法,看完本文您将对二分法有一个全新的认识。(ps: 有二分基础的可以直接看二分法的两种写法,里面分享的是二分的边界处理细节问题)

1 二分法原理

  说到原理,其实我们在上初中的时候就已经接触过它的思想了。

  莫慌莫慌😁我放一张图你就会想起来

对于区间[a,b]上连续不断且f(a)·f(b)<0的函数y=f(x),通过不断地把函数f(x)的零点所在的区间一分为二,使区间的两个端点逐步逼近零点,进而得到零点近似值的方法叫二分法。

  相信阁下已经想起来第一次接触二分思想的时候了哈哈!

1.1 定义一

其中维基百科上是这样定义二分的

二分法(dichotomy)指的是将一个整体事物分割成两部分。也即是说,这两部分必须是互补事件,即所有事物必须属于双方中的一方,且互斥,即没有事物可以同时属于双方。

1.2 定义二

百度百科是这样定义二分的

二分法(Bisection method) 即一分为二的方法. 设[a,b]为R的闭区间. 逐次二分法就是造出如下的区间序列([an,bn]):a0=a,b0=b,且对任一自然数n,[an+1,bn+1]或者等于[an,cn],或者等于[cn,bn],其中cn表示[an,bn]的中点。

  相信读者朋友可以看出来维基百科上的定义更具有普遍性。我们不妨以猜数字游戏引出计算机领域的二分法的用处。

假如这里有1-10的数字,游戏玩家预先抽出一张数字,要求是使用最少的次数猜中抽出的数字 (ps:在猜测期间,游戏玩家会得到所猜数字是猜大了还是猜小了.)

  这样我们猜测的时候就可以使用二分的思想进行猜测

  • 首先,猜测 1-10 的中间数字 5 ,如果猜大了, 6-10 的数字就不可能是答案,答案只可能存在于 1-4 ,反之,答案存在于 6-10 .
  • 接着,我们继续猜测 1-5 (6-10) ,选择 1-5 的中间值 3 ,如果猜大了, 4-5 的数字就不可能是答案,答案就只可能存在于 1-2 中
  • 以相同的方式就可以寻找到答案.
      下面用图解释一下

  从上面猜数字游戏中我们可以尝试使用维基百科上的定义套一下
为什么我们可以折半猜测数字呢? 在猜测 mid == 5 时,若猜大了,为什么 5-10 的数字一定不是答案,其实这里面隐含着一个东西 : 有序 ,维基百科上说的互斥且互补,因为答案一定在 1-5 或 6-10 中,且不会同时存在两个区间当中,1-10 这十个数字是有序排列的,有序满足了互补且互斥,所以可以使用二分猜测数字.

在其他博主中也有这样描述二分法的

  • 满足二段性
  • 答案在二段性的分界点

其实本质上是一样的.

  分享到这,我想说的是二分法的思想很简单,但难在细节的处理上,边界问题,这关系到区间收敛的成功与否,在写本片博客时,我也刚刚认真对待二分的边界问题,之前就是学个思想,只记了一种闭区间二分写法,几乎没有二分边界的意识.下面我就深入分享一下二分的边界问题,这也是二分的精髓所在.

2 二分法的两种写法

Leetcode T34 为例.

  • 非递减(说明是有序的,可以使用二分来做)
  • 寻找区间(目标值在数组中的开始位置和结束位置)

  相信读者见过有些代码的while循环终止条件是 \(left <= right, [left,right]\); 有的是 \(left < right,[left,right)\) , 其实这就是今天要分享的重点之一 收敛点的处理方式 .

2.1 写法一 闭区间 \(left <= right\)

 def binarySearch(nums:List[int], target:int) -> int:
        left, right = 0, len(nums)-1
        while left <= right: # 不变量:左闭右闭区间
            mid = left + (right-left) //2 
            if nums[mid] < target: 
                left = mid + 1
            else: 
                right = mid - 1
        return left  # 若存在target,则返回第一个等于target的值 
  • 为什么 \(left <= right\) 时就是闭区间呢?
    因为当 \(left = right\) 时仍然有意义, 此时 \(mid\) 被排除在区间意外,不会包括。
  • 为什么 \(nums[mid] >= target\)\(right = mid - 1\) 如果这样操作不是把等于正确答案的也排除在外了吗?
    非也非也。当 \(nums[mid] = target\) 那一刻,$ right $ 确实变成 $ mid - 1 $了,这也就意味着 \(right\) 此时在 \(target\) 的左边,紧挨着 \(target\) ,此时 \(right\) 就不会在动了,\(left\) 会一直向 \(right\) 赶来,直到二者相遇,又因为 \(left = right\) 有意义,比较完之后,因为此时的 \(nums[right] < target\) ,故 \(left = mid + 1\) 也就是 \(left\) 再次向前移动一位,移到了 \(target\) 位置,此时 \(left <= right\) 不成立, \(return\)\(left\) 即是正确答案。

2.2 左闭右开区间 \(left < right\)

 def binarySearch(nums:List[int], target:int) -> int:
        left, right = 0, len(nums)-1
        while left < right: # 不变量:左闭右闭区间
            mid = left + (right-left) //2 
            if nums[mid] < target: 
                left = mid + 1
            else: 
                right = mid
        return left  # 若存在target,则返回第一个等于target的值 
  • 为什么 \(left < right\) 时就是左闭右开区间呢?
    因为当 \(left = right\) 时没有意义(不满足while循环条件), 此时 \(mid\) 包含在区间内,即 \(nums[mid]\) 有可能是答案。

\(nums[mid] = target\) 那一刻,$ right $ 确实变成 $ mid$ 了,这也就意味着 \(right\) 此时在 \(target\) 的位置(可能是第一个,可能不是),此时如果再遇到 \(nums[mid] = target\)\(right\) 还会再动,\(left\)\(right\) 会一直靠近,直到二者相遇

if nums[mid] < target: 
      left = mid + 1

大家再看这一句,$ left $ 把边界把的死死的,只要是小于 \(target\) 的值一律排除在外,所以当 \(left\)\(right\) 相遇时,所指一定是 \(target\) (当然,前提是数组里面存在 \(target\)

大家会发现笔者是这样判断的

if nums[mid] < target: 
     left = mid + 1
else: 
     right = mid
  • 即当 \(nums[mid] < target\) 时,\(left = mid + 1\) ,大家会发现 $ left $ 把边界把的死死的,只要是小于 \(target\) 的值一律排除在外。
  • \(nums[mid] >= target\) 时,$right = mid $,大家会发现 $ right $ 更加宽容一些,相等时也会移动,这就意味着 $ right $ 在向着 $ left $ 靠近,没错,这其实倾向于寻找存在重复元素时,第一个出现的元素。

如果找最后一个呢?
  莫慌莫慌,这可以转换为寻找 \(target + 1\) 的第一个出现的位置,然后下标减一,这不就是 \(target\) 的最后一个元素了吗哈哈哈🌞

end = binarySearch(nums, target + 1) - 1

2.3 我们再来反证下面两个问题

2.3.1 为什么 \(while\) \(left <= right:\) 搭配 \(right = mid + 1\)

  如果使用循环终止条件 \(while\) \(left <= right:\), 且 \(right = mid\) ,当 \(left\)\(right\) 同时指向同一个 \(target\) 的时候,就死循环了。

2.3.2 为什么 \(while\) \(left < right:\) 搭配 \(right = mid - 1\)

  如果使用循环终止条件 \(while\) \(left < right:\), 且 \(right = mid + 1\) ,当出现下图所示的情况时,就把 \(target\) 完美漏掉了。而且由于循环终止条件是 \(while\) \(left < right:\),无论如何 \(left\) 也不会跑到 \(right\) 右边去,因为二者相等时就 \(return\) 了。

2.4 当使用左闭右开区间 \(left < right\) 时,向上(下)取整问题

  下面以两种情况为例。

2.4.1 求“满足条件”的最小值

把一些会玩王者荣耀的小朋友按照水平由低到高排好序,想要从中找到能够打败小明的水平最差的那个小朋友。(这里假设水平高一些的一定可以打败水平低一些的,大家都知道,队友坑人的话,那 \(5v5\)哪是 \(5v5\) 啊,那分明是 \(1 v(5+n))\)

while l < r :
    mid = (l+r) >> 1
    if check(mid) :
        r = mid
    else :
        l = mid + 1

其中 $check(mid)¥是一个返回值为 \(bool\)类型的函数,当序号为 \(mid\) 的小朋友可以打败小明时,返回为 $ true$ ;否则返回 \(false\)
我们仔细分析一下这几行代码,首先是第一条原则:答案一定在 \([ l,r ]\) 中。

  • \(check(mid)为true\) 时,说明 \(mid\) 可以打败小明,那么序号大于 \(mid\) 的一定也可以,而题目要找的是能打败他的水平最差的,所以正确答案一定在序号小于等于 \(mid\)(可能是\(mid\))的一侧,因此令 \(r = mid\) 而不是 \(r = mid - 1\)
  • \(check(mid)为false\) 时,说明 $ mid$ 已经无法打败小明,那么序号小于等于mid的小朋友都无法打败小明,正确答案一定在 \([mid + 1,r]\) 这个区间内,所以置 \(l = mid + 1\),而不是 \(l = mid\).

2.4.2 求“满足条件”的最大值

把一些会玩王者荣耀的小朋友按照水平由低到高排好序,想要从中找到不能打败小明的水平最高的那个小朋友。(这里仍然假设水平高一些的一定可以打败水平低一些的)

while l < r :
    mid = (l + r + 1) >> 1
    if check(mid) :
        r = mid - 1
    else :
        l = mid

注意其中的改变了的地方,我们来分析为什么要变为这样,首先仍然是第一条原则:答案一定在\([ l,r ]\)中。

  • \(check(mid)为true\)时,说明 \(mid\) 可以打败小明,那么序号大于 \(mid\) 的一定也可以,而题目要找的是不能打败他的水平最高的,所以正确答案一定在序号小于mid(不包括mid)的一侧,因此令 $ r = mid - 1$ 而不是 $ r = mid$.
  • \(check(mid)为false\) 时,说明 \(mid\) 已经无法打败小明,那么序号小于等于 \(mid\) 的小朋友都无法打败小明,但是我们又要找不能打败他的水平最高的那个,所以正确答案一定在\([mid,r]\) 这个区间内(可能是 \(mid\)),所以置 \(l = mid\),而不是 \(l = mid + 1\).

注意这里变成了向上取整的方式 \(mid = (l + r + 1) >> 1\) , 原因如下:
如果 \(l、r\) 在某轮循环中分别是 \(2、3\),则 \(mid = 2\) ,若 \(check(mid)为false\),则 \(l = mid\),你会发现,咦?此轮循环后,\(l\)\(r\) 的值都没变,不仅如此,从此往后每一次循环它们都不会再变了!死循环他来了!

其实不仅是第二种情况不正确的mid取值方式可能会陷入死循环,第一种也会,如果我们让第一种的mid取值方式改为 \(mid = (l + r + 1) >> 1\),同样存在陷入死循环的可能,仍然用上面2、3的例子,此时 \(mid =(2 + 3 + 1)>> 1\),即 \(mid = 3\),而如果 \(check(3)为true\),则\(r = mid = 3\),再次陷入死循环。

  也就是说,如果是r=mid,则mid应该向下取整;如果是l=mid,则要向上取整.

3 总结

  好了,以上就是笔者想要分享的关于二分法的全部内容了,若有错误,请大佬们不吝评论区赐教,小生当俯身倾耳以请😁。希望笔者的分享能够帮助到您!

参考

  1. https://zhuanlan.zhihu.com/p/275995132?utm_id=0
  2. https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%B3%95
  3. https://blog.csdn.net/qq_45734984/article/details/120331469?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-120331469-blog-122775097.235%5Ev38%5Epc_relevant_anti_t3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-120331469-blog-122775097.235%5Ev38%5Epc_relevant_anti_t3&utm_relevant_index=2
posted @ 2023-08-19 00:03  荒_ayang  阅读(540)  评论(0)    收藏  举报