系列合集:
二分算法(一) 新手可看,图解二分入门
二分算法(二) 二分答案入门
二分答案思想
和二分查找相似,二分查找在一个有序范围内查找某个值并返回,二分答案就是在有序范围内查找答案。需要满足以下条件(答案写成 ans):
- 条件一:ans 在一个范围内,没范围就没法二分查找了
- 条件二:ans 范围里面的元素具有二分性
- 条件三:每个可能的 ans 是可以验证是否有效的
假设我们能知道问题的答案一定在某个区间 v 内,且区间具有二分性质,就可以使用二分思想了。至于什么是二分性,我不知道怎么解释,我简单理解成可以将数组划分为两种状态。
练习一
问题:287. 寻找重复数
分析
分析:以 [1,3,4,2,2] 这个用例来分析, n == 4,有 n + 1 个数字。
- 条件一:很显然答案是有范围的一定在 [1, n] 这个范围内,这个用例就是 [1, 4],这里满足了上述条件一。
- 条件二:这个范围是否具有二分性呢,通过分析可以发现,如果统计 [1,3,4,2,2] 中小于等于每个元素的元素个数,那么最后答案是有单调性的,比如:统计 <= 1 的元素,有 1 个,<= 2 的元素有3 个,<= 3 的元素有 4 个,<= 4 的元素有 5 个。将 <= x 的元素个数计为 cnt,那么 就有两种状态(分别用蓝色和红色表示),一种是 x <= cnt(x 在 ans 的左侧,即 x < ans),另一种是 x > cnt (x 为 ans 或者 ans 的右侧,即 x >= ans),具有二分性。
- 条件三:给定一个 x ,根据上述分析,通过统计 <= x 的元素个数,就可以得出 x 在 答案的左侧还是右侧。
- 我们已经将答案数组划分为蓝色(< ans)和红色(>= ans)了,最后 ans 就是红色的第一个,假设 L 的左侧元素都 < ans,R 自身和右侧元素都 >= ans,那么就可以得出变化关系。
- 当 check(mid) 时,R = mid
- 当 !check(mid) 时,L = mid + 1
- check(x) 表示 x 是否 >= and,我们需要自行补充,对这个二分变化不熟悉的可以看这一篇,简单易懂 二分算法(一) – 新手可看,图解二分入门
实现
半开半闭区间写法
class Solution {
public:
int findDuplicate(vector<int>& nums) {
// 假设 l 左侧均 < ans,r 及其右侧均 >= ans
int n = nums.size(), l = 0, r = n, mid;
// x 这个数是否 >= ans
function<bool(int)> check = [&](int x) -> bool {
int cnt = 0;
// 统计 <= x 的元素数量
for(int i : nums) cnt += i <= x;
return cnt > x;
};
while(l < r) {
mid = l + (r - l) / 2;
if(check(mid)) r = mid;
else l = mid + 1;
}
// 返回 l 或者 r 都可以
return l;
}
};
双开区间写法
也可以假设L及其左侧都 < ans,R 及其右侧都 >= ans,不清楚这种写法的可以看 二分算法(一) – 新手可看,图解二分入门 ,图解如下所示:
- 二分循环条件需要改变,如上图所示,当 L 和 R 相邻的时候就结束了,条件就改为 while(l + 1 < r),根据假设,L 和 R 的初始值为 -1 和 n。
- L 和 R 变化规则变了
- 当 check(mid) 时,R = mid
- 当 !check(mid) 时,L = mid
- 上述变化维护了 L 和 R 的假设
- 如上图所示,结果返回 R
class Solution {
public:
int findDuplicate(vector<int>& nums) {
// 假设 l 及其左侧均 < ans,r 及其右侧均 >= ans
int n = nums.size(), l = -1, r = n, mid;
// x 这个数是否 >= ans
function<bool(int)> check = [&](int x) -> bool {
int cnt = 0;
// 统计 <= x 的元素数量
for(int i : nums) cnt += i <= x;
return cnt > x;
};
while(l + 1 < r) {
mid = l + (r - l) / 2;
if(check(mid)) r = mid;
else l = mid;
}
// 返回 r
return r;
}
};
练习二
分析
- 条件一:答案有范围,ans 一定在 [sum / m, sum] 里(sum 表示 nums 的求和)。
- 条件三:能否判断 nums 是否可以分为 m 个子数组,且每一个子数组的和 <= x,我们可以写一个 check(x) 来判断
- 条件二:可以发现 ans 是刚好满足 check(ans) 的,若 x < ans,那么 check(x) == false;若 x >= ans,那么 check(x) == true。就可以把答案数组 [sum / m, sum] 分为两份,一份 < ans,另一份 >= ans。
实现
半开半闭写法
假设 L 左侧元素都 < ans,R 及其右侧元素都 >= ans,初始值为 sum / m 和 sum,变化关系为:
- 当 check(mid) 时,r = mid
- 当 !check(mid) 时,l = mid + 1
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int sum = accumulate(nums.begin(), nums.end(), 0);
int l = sum / m, r = sum, mid;
// 二分答案, l 左侧代表不可以,r 及其右侧代表可以,l 和 r 相邻代表所有的数字都判断过了
while(l < r) {
mid = l + (r - l) / 2;
if(check(nums, mid, m)) r = mid;
else l = mid + 1;
}
return r;
}
// 分成 k 段, 判断是否每段都可能 <= x
bool check(vector<int>& nums, int x, int k) {
int tmp = 0, cnt = 0, i, n = nums.size();
for(i = 0; i < n && k - 1 >= 0; ) {
if(tmp + nums[i] > x) {
k--;
tmp = 0;
} else {
tmp += nums[i++];
}
}
return k - 1 >= 0;
}
};
双开区间写法
假设 L 及其左侧元素都 < ans,R 及其右侧元素都 >= ans,初始值为 sum / m - 1 和 sum,循环条件是 while(l+1 < r),变化关系为:
- 当 check(mid) 时,r = mid
- 当 !check(mid) 时,l = mid
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int sum = accumulate(nums.begin(), nums.end(), 0);
int l = sum / m - 1, r = sum, mid;
// 二分答案, l 及其左侧代表不可以,r 及其右侧代表可以,l 和 r 相邻代表所有的数字都判断过了
while(l + 1 < r) {
mid = l + (r - l) / 2;
if(check(nums, mid, m)) r = mid;
else l = mid;
}
return r;
}
// 分成 k 段, 判断是否每段都可能 <= x
bool check(vector<int>& nums, int x, int k) {
int tmp = 0, cnt = 0, i, n = nums.size();
for(i = 0; i < n && k - 1 >= 0; ) {
if(tmp + nums[i] > x) {
k--;
tmp = 0;
} else {
tmp += nums[i++];
}
}
return k - 1 >= 0;
}
};