- 编程
二分查找边界的总结
- 2025-5-2 11:50:12 @
老师给大家总结了常见二分,把各种常见二分场景拆成两大类:离散二分(在整数或下标上)和连续二分(浮点/实数),并且针对「查找第一个满足条件的位置」与「查找最后一个满足条件的位置」两种子问题,列出它们的循环条件和更新边界的套路。
一、离散二分(整数/下标)
假设我们在一个单调布尔数组或区间 \([L_0,R_0]\subseteq\mathbb Z\) 上,定义条件函数 good(mid)
:若 good(mid) == true
,说明“答案”在 \(\le mid\) 侧(或反过来,下面会分情况)。
情形 A:查「第一个使 good(mid)==true 的下标」
- 区间:\([L_0,R_0]\),约定
good(x)==false
在左,true
在右,形如false, false, …, false | true, true, …, true ↑ answer
- 模板:
int l = L0, r = R0; // 不要用 <=,否则 mid = (l+r)/2 可能陷入死循环 while (l < r) { int mid = l + (r - l) / 2; if (good(mid)) { // 答案可能就是 mid,也可能在更左 r = mid; } else { // mid 及其左边都不行,只能往右 l = mid + 1; } } // 退出时 l == r,且就是第一个 good()==true 的位置 return l;
- 为什么:
l<r
保证区间长度至少 2,每次至少缩一点;r=mid
保留 mid,因为 mid 可能就是第一个 true;l=mid+1
跳过所有 false,包括 mid 本身。
情形 B:查「最后一个使 good(mid)==true 的下标」
- 区间:\([L_0,R_0]\),约定
true
在左,false
在右,形如true, true, …, true | false, false, …, false ↑ answer
- 模板:
int l = L0, r = R0; while (l < r) { // 向上取整中点,避免 l 不动导致死循环 int mid = l + (r - l + 1) / 2; if (good(mid)) { // mid 及其左边都是 true,答案在 [mid, r] l = mid; } else { // mid 一定不行,只能往左走 r = mid - 1; } } // 返回 l(==r) return l;
- 关键:
mid = l + (r-l+1)/2
保证当l+1==r
时,mid 总是r
,让l=mid
能缩区间;l=mid
保留 mid,因为它可能是真正的最后一个 true;r=mid-1
跳过 mid 及其右侧所有 false。
小结离散二分
目标 | 循环条件 | mid 取法 | good(mid)==true 时 | good(mid)==false 时 |
---|---|---|---|---|
第一个 true |
l<r |
(l+r)/2 |
r = mid |
l = mid+1 |
最后一个 true |
(l+r+1)/2 |
l = mid |
r = mid-1 |
- 千万别用
while(l <= r)
搭配这两组写法,否则要么多一次迭代要么死循环;l<=r
更常用于「在区间内找某个固定值,返回-1
或下标」的场景,搭配l = mid+1
/r = mid-1
。
二、经典的 while (l <= r)
模板
当你想「在区间内查找整型值 x
是否存在且返回下标」,或者「枚举 mid
,一旦 good(mid)
就 break
」,就用这种:
int l = L0, r = R0;
while (l <= r) {
int mid = l + (r - l) / 2;
if (A[mid] == target) {
return mid;
} else if (A[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
// 没找到
return -1;
l<=r
保证所有下标都被试到;l=mid+1
和r=mid-1
保证区间在每次迭代都严格收缩。
三、连续二分(浮点/实数)
常用于「在实数区间上找一个满足单调条件的边界」,比如最小化/最大化函数值等。一般写:
double l = L0, r = R0;
for (int it = 0; it < ITER; it++) {
double mid = (l + r) / 2;
if (good(mid)) {
r = mid; // 或者 l = mid,视具体找左边界还是右边界
} else {
l = mid;
}
}
double ans = (l + r) / 2;
- 取
ITER
足够大(如 60 次)或用while (r-l > eps)
。 - 更新时用
l=mid
、r=mid
(不加减 1,因为这里不是离散的,mid±eps
本质上也是这样做)。
四、如何快速选用套路
- 找下标/整数的第⼀个/最后⼀个 ⇒ 用「
while(l<r)
, 分两种 mid 取法」的 情形A/B。 - 判断某值是否存在并返回下标 ⇒ 用「
while(l<=r)
,l=mid+1
/r=mid-1
」。 - 找实数最优点 ⇒ 用「
while(r-l>eps)
或固定迭代次数 ,l=mid
/r=mid
」。
掌握这三类,就能应对市面上几乎所有二分问题。只要记住:
l<r
用于「缩区间到只剩一个答案」;l<=r
用于「在区间里找某个值/突破平局」;- mid 向下/向上取整 决定保留哪端。
祝你边界无忧,二分必胜!
1 条评论
-
xrqmzl LV 1 SU @ 2025-5-2 12:19:48
连续二分”与“离散二分”的区别:
✅ 关键区别总结:
对比维度 离散二分 连续二分 定义域 在整数、数组下标等 离散值域 上 在实数范围、浮点区间等 连续值域 上 数据本质 数据点是分开的:1、2、3... 数据是密集的:1.0001、1.0002... 终止条件 l == r
或l > r
(整数精确)r - l < eps
或迭代次数足够mid 计算 mid = (l + r)/2
(整型)mid = (l + r)/2.0
(浮点)是否能枚举 能穷举(如 1~n) 不能枚举(如 ([0, \sqrt{2}]))
🎯 举例说明
🌰 示例 1:离散二分(整数域)
题目:给定一个单调非减数组
a
,找第一个 ≥target
的位置。int l = 0, r = n - 1; while (l < r) { int mid = (l + r) / 2; if (a[mid] >= target) r = mid; // 答案可能在左边或 mid 本身 else l = mid + 1; }
- 数组下标是 离散值域,比如位置 3、4、5 是分开的。
- 退出条件是
l == r
,可精确找到下标。
🌰 示例 2:连续二分(实数域)
题目:给定函数 (f(x) = x^2),在区间 ([1, 10]) 上找到最小的 (x) 满足 (f(x) \geq 7)。
double l = 1.0, r = 10.0; while (r - l > 1e-8) { double mid = (l + r) / 2.0; if (mid * mid >= 7) r = mid; else l = mid; }
- 实数不能穷举,只能逼近,因此是连续值域。
- 退出条件是
r - l < eps
,精度控制。 - 无法做到
l == r
,因为实数之间永远还有更小差距。
🔍 连续与离散二分本质差异再强调:
离散二分:
- 适用于:数组下标、整数范围、可穷举空间。
- 可以精确命中“位置”或“值”。
连续二分:
- 适用于:浮点数区间、解方程、优化问题中搜索实数解。
- 只能逼近答案,通常靠误差小于 eps 结束。
✅ 一句话总结:
离散二分是找“具体的点”,连续二分是逼近“某个实数”。
希望这个区分让你对两者有了真正的掌握!如果需要我可以帮你整理一个表格挂墙记忆版 🙂
- 1