系列:
二分算法(一) 新手可看,图解二分入门
二分算法(二) 二分答案入门
核心在于划分区间并确定返回值,然后再确定 L 和 R 是什么,最后维护状态设置变化规则。将在下面两个简单的练习中给出解释。
练习一
方式1
问题1:递增序列 v 中找第一个大于等于x的下标。
既然有单调性,就可以使用二分的思想,以找到第一个大于等于 target 的下标为例。我们可以从结果出发,想象最后这个序列是分成两半的,问题就变成如何去划分,结果又怎么从这个划分好的数组中得出。
- 因为要找第一个大于等于 target 的下标,我们将其划分为两半,一边是小于 target 的元素,另一边是大于等于 target 的元素,那么答案 ans 就是红色第一个元素了。
- 确定了答案位置之后,就可以确定 l 和 r 了,如果令最后 l == r(半开半闭),那么 l 和 r 就指向同一个地方,假设都指向 ans,那么 l 的左侧元素都 < target,r 自身和右侧元素都 >= target,我们在后续的变化中需要维护这个规律/状态。
- 最后一步根据上述假设写出变化:
- 当 v[mid] < target,令 l = mid + 1;l 左侧仍然是 < target 的
- 当 v[mid] >= target,令 r = mid;r 和 r 右侧仍然是 >= target 的
- 以上两步均维护了状态不变,这种是半开半闭,虽然我觉得这个名称不重要甚至会误导。
关键在于假设 L 和 R 的 状态( L 的左侧所有元素的值均小于 x,R 自身和右侧的元素均大于等于 x),并在变化中 维护状态 不变。
代码实现
// 找到第一个大于等于 x 的下标
int lower_bound(vector<int> v, int x) {
int n = v.size();
int l = 0, r = n, mid;
while(l < r) {
mid = l + (r - l) / 2;
if(v[mid] < x) l = mid + 1; // 变化后l左侧仍然都是小于x的
else r = mid; // 变化后r和r右侧都仍然是大于x的
}
return l == n ? -1 : l;
}
方式2
划分方式不止一种,也可以如此划分:
这种划分的答案就是 r 。
- l 及其左侧元素都 < target,r 及其右侧元素都 >= target。
- 当 l 和 r 相邻的时候结束,即 while(l + 1 < r)。初始值设置为 l = -1,r = n(l 不能取0,因为还没判断v[0] 是否满足 < target,同理,r 不能取 n - 1)。
- 当 v[mid] < target,令 l = mid
- 当 v[mid] >= target,令 r = mid
- 这样维护了状态不变,一般也叫开区间写法。
// 找到第一个大于等于 x 的下标
int lower_bound(vector<int> v, int x) {
int n = v.size();
int l = -1, r = n, mid;
while(l + 1 < r) {
mid = l + (r - l) / 2;
if(v[mid] < x) l = mid;
else r = mid;
}
return r == n ? -1 : r;
}
练习二
判断第一个位置:
- 划分数组,一边是 < target,另一边是 >= target,那么红色部分的第一个元素就是答案ans,使用半开半闭写法,最后 l == r。
- 假设 l 和 r 都指向 ans
- 那么 l 左侧的元素都 < target
- 那么 r 及其右侧元素都 >= target
- 根据上述假设写出变化:
- 当 v[mid] < target,令 l = mid + 1;
- 当 v[mid] >= target,令 r = mid;
- 以上两步均维护了状态不变
判断最后一个位置:
target的位置,可以调用上面的方法,查找第一个 target + 1,然后把答案-1返回,或者再写一个也可以(很容易错):
- 划分区间,ans 就是 L - 1 或者 R - 1,
- 确定 l 和 r 的含义
- l 左侧所有元素都 <= target
- r 及其右侧元素都 > target
- 变化
- 当 v[mid] <= target,l = mid + 1
- 当 v[mid] > target,r = mid
- 以上两步均维护了状态不变
这里补充一个细节:
求最后一个位置有两种可能的写法:
- 方法1:就是上面的图,划分为 l 左侧元素都 <= target,r 及右侧元素都 >= target。变化就是 l = mid + 1;r = mid; 返回 l - 1
- 方法2: 划分为 l 及左侧元素都 <= target,r 右侧元素都 > target。l = mid;r = mid - 1;返回 l
- 这种情况下方法1是对的,2 是错的,因为mid = (l + r) / 2,是向下取整的。有可能出现 l 和 r 相邻,然后mid就等于l,那么如果 v[mid] <= target,那么 l = mid 将没有发生变化,出现 死循环 !!!,如下所示,永远无法跳出循环 while(l < r)。
Tip
因为 mid 向下取整的特性,所以应该使用 l 左侧的元素满足xx,r 及右侧元素满足xx的写法。
代码实现
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int n = nums.size(), l ,r, mid;
if(nums.size() == 0) return {-1, -1};
// l 左侧都 < target,r 及右侧 >= target
l = 0, r = n;
while(l < r) {
mid = l + (r - l) / 2;
if(nums[mid] < target) l = mid + 1;
else r = mid;
}
// 判断是否存在
if(r == n || nums[r] != target) return {-1, -1};
int first = r;
// l 左侧 <= target , r 及其右侧 > target
l = 0, r = n;
while(l < r) {
mid = l + (r - l) / 2;
if(nums[mid] <= target) l = mid + 1;
else r = mid;
}
// 因为上面判断过了第一个target存在,所以最后一个target一定存在
return {first, l - 1};
}
};