二分法
二分法是在有序区间中以 O(log n) 复杂度查找答案的方法。
不仅适用于:
- 完全有序数组
- 部分有序数组
- 单调函数
- 答案空间搜索
例如:
- LeetCode 704:二分查找
- LeetCode 34:查找元素第一个和最后一个位置
- LeetCode 33:搜索旋转排序数组
- LeetCode 153:寻找旋转数组最小值
- 各种「答案二分」
虽然二分法思想很简单,但是对于不同二分变形有很多细节问题。
二分法细节
二分法真正困难的不是思想,而是:
- 区间定义
- 循环条件
- mid 取值方向
- 区间更新方式
- 死循环问题
所有二分模板,本质上都在维护:
精确查找
使用的是左闭右闭区间,即
每次更新中点时使用
1 2 3 4
| mid = left + (right-left) / 2; 或者 mid = (left + right) >>> 1; 即向下取整
|
每次更新左右端点时使用
1 2 3
| left = mid + 1; 或者 right = mid - 1;
|
适用场景: 数组无重复元素,或者你只需要随便找一个等于 target 的下标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public int binarySearchExact(int[] nums, int target) { if (nums == null || nums.length == 0) return -1; int left = 0; int right = nums.length - 1;
while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; }
|
第一个满足条件的位置/下界/上届
循环条件
采用左闭右开区间
此时初始化为
因为右开,所以不会越界
当left=right时,此时为空区间。
更新方式,因为不包含right位置的元素,所以
下界(第一个等于 target 的位置)
适用场景: 数组中有重复元素,要求找第一个(最左边)等于 target 的下标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public int binarySearchLeftBoundary(int[] nums, int target) { if (nums == null || nums.length == 0) return -1; int left = 0; int right = nums.length - 1;
while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] >= target) { right = mid; } else { left = mid + 1; } } return nums[left] == target ? left : -1; }
|
上界(查找最后一个等于target的位置)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public int binarySearchRightBoundary(int[] nums, int target) { if (nums == null || nums.length == 0) return -1; int left = 0; int right = nums.length - 1;
while (left < right) { int mid = left + (right - left + 1) / 2; if (nums[mid] <= target) { left = mid; } else { right = mid - 1; } } return nums[left] == target ? left : -1; }
|
核心规律
1.闭区间
配套
1 2
| right = n - 1; while (left <= right)
|
因为
2.左闭右开
配套
1 2
| right = n; while (left < right)
|
因为
3.更新left=mid
4.更新right=mid
5.right = mid 和 right = mid - 1 的区别
取决于
1
| mid 是否仍然可能是答案,是的话right = mid 否则 right = mid - 1
|
答案二分
答案二分的本质
不是在数组里二分元素。
而是在“答案空间”里二分最终结果。
很多题目根本没有“有序数组”,
但:
于是仍然可以二分。
例子:分割木头
1 2 3 4 5 6 7
| 有若干木头, 每段长度为 nums[i]
要求切成至少 k 段。
问: 每段最大能有多长?
|
如果:
那么:
一定也可行。
于是:
1
| true true true true false false
|
出现了。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public int maxLength(int[] nums, int k) {
int left = 1; int right = Arrays.stream(nums).max().getAsInt();
while (left < right) { // 这里是找上界 // 找最大可行值 // 所以这里必须上取整 int mid = left + (right - left + 1) / 2;
if (check(nums, k, mid)) { left = mid; } else { right = mid - 1; } }
return left; }
private boolean check(int[] nums, int k, int len) {
int count = 0;
for (int x : nums) { count += x / len; }
return count >= k; }
|
答案二分的本质
其实是:
转化为:
最大值最小化
经典:
例如:
二分:
check:
经典:
例如:
1 2
| 洛谷 aggressive cows LeetCode 1552
|
二分:
check:
什么时候能想到答案二分
看到:
例如:
- 答案范围巨大
例如:
暴力枚举不可能。
例如:
或者:
答案二分真正的数学本质
本质上是在二分:
答案二分模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int left = 最小可能答案; int right = 最大可能答案;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (check(mid)) { left = mid; } else { right = mid - 1; } }
return left;
|