茴香豆的茴有四种写法,那二分有几种写法?

《编程珠玑》一书的作者 Jon Bentley 曾经说过:“90%的程序员无法正确实现二分查找算法...”,今天,本文将带领你会写二分。

经典写法

现在我们来求解这样一个通用的二分查找问题:有一个不下降序列 $ a $,我们要从其中所有找到大于等于 $ k $ 的数的最小的下标。

bool check(int index) {return a[index] >= k;}

while (l < r) {
	int m = (l + r) / 2; // 或 l + (r - l) / 2 防止溢出
	check(m) ? (r = m) : (l = m + 1);
} return l;

序列一定可以划分为两部分,左半部分全部小于 k, 右半部分全部大于等于 k,我们找到右半部分的第一个元素。

l 和 r 通常赋初值为 0 和 数组末下标。接下来,已知答案在 \([l, r]\) 内,首先选取一个靠中的位置 $ m $,若此处已经满足大于等于条件,则他位于右半边,答案应位于 m 左侧或 m 本身,所以我们修改新的可能区间为 \([l, m]\);反过来,若此处不满足大于等于条件,则他位于左半边,答案应位于 m 右侧(不可能包含其本身),所以我们修改新的可能区间为 \([m+1, r]\)

当 l 距离 r 仅 1 时,int m = (l + r) / 2将向零取整得到 l, 若其可行则 r 会被赋值 l,否则 l 会被赋值 r,无论情况,最终都会使得 l 和 r 相等,循环结束。此时 l (或 r)就是正确答案。

处理例外情况

需要特别说明的是,当 \(k\) 比序列中最大的数还大时,不存在符合条件的数,得到的结果无效,二分查找的结果需要进行验证。通常来说,如果下标 0 开,初始 l = 0, r = len,查找到的 \(l\) 就会超出序列的范围。因此,在使用二分查找后,可以检查 \(l\) 是否在有效范围内并且 \(a[l] \ge k\),以确保找到的答案有效,或者在二分查找前先将 $ k $ 和末数对比。

另一种经典写法

接下来,我们讨论另一种经典写法。假设问题变为:我们仍然有一个不下降序列 \(a\),但是这一次我们需要找到其中小于等于 \(k\) 的最大下标。

bool check(int index) {return a[index] <= k;}

while (l < r) {
	int m = (l + r + 1) / 2; // l + (r - l + 1) / 2
	check(m) ? (r = m - 1) : (l = m);
} return l;

和上文类似,在每次循环中,如果位置 \(m\) 满足条件 \(a[m] \le k\),说明 \(m\) 是可行的,因此我们将 \(l\) 赋值为 \(m\),即答案可能出现在当前 \(m\) 位置或更右侧;如果 \(m\) 不满足条件,则说明 \(m\) 太大,因此将 \(r\) 赋值为 \(m - 1\),在下一次循环中继续查找左侧部分。

但是,在更新中间位置 \(m\) 时,使用 int m = (l + r + 1) / 2,即向上取整。当 l 距离 r 仅 1 时,m 将被赋为 r, 若其可行则 l 会被赋值 r,否则 r 会被赋值 l,循环可以正常结束。

两个问题的根本区别在于可行域所在的位置不一样,一个是可行域在左侧,求其右端;另一个则是可行域在右侧,求其左端。这导致了检查 m 后对 l 和 r 修改的不同,又进一步导致 m 不在正中央时的取整条件不同,确保循环在最后一步正确结束而不是无限循环。因此,两套方案一一对应,不可以混用。

下标取整问题

我们现在知道“若 l = m + 1,则取 m 应向下;若 r = m - 1,则取 m 时应向上”。不过 (l + r) / 2 在 C++ 中是向零取整的。这就意味着在 l 和 r 可取负数时,结果将不正确,m 会向上取造成死循环。解决方案之一是改用 l + (r - l) / 2,而对于写法二,或者其他取整规则不同的语言,也可自行推导。关键不是死记硬背形式,而是记住如何取整。

带记录答案的写法

int ans;
while (l <= r) {
	int m = (l + r) / 2;
	check(m) ? (ans = m, r = m - 1) : (l = m + 1);
} return ans;

无论 m 是否可行,新的区间里总是不取它,而在每次 m 可行时单独记录一次。这样的话 m 向任何一方取整都可以。同时,无论如何取整都会出现,r - l 跳过 1 直接达到 0 的情况,因此 l = r 时还需要再循环一次,最终结束时 l > r,ans 是答案。

这种写法无视可行域的方向,但是需要一个额外的变量。

posted @ 2024-10-19 23:21  Ofnoname  阅读(19)  评论(0编辑  收藏  举报