二分查找 三分查找与二分答案
二分查找
二分查找的前提
目标函数具有单调性(单调递增或者递减)
存在上下界(bounded)
能够通过索引访问(index accessible)
示例
在单调递增数组里
[10, 14, 19, 26, 27,31, 33, 35, 42, 44]
查找: 33
返回33所在的下标
C++ / Java代码模板
int left=0,right=n-1;
while (1eft <= right) {
int mid = (1eft + right) / 2;
if (array[mid] == target)
// find the target!
break or return mid;
if (array[mid] < target)
left=mid+1;
else
right = mid - 1;
}
Python代码模板
left, right = 0,len(array) - 1
while left <= right:
mid = (left + right) // 2
if array[mid] == target:
# find the target!
break or return mid
elif array[mid] < target:
left=mid+1
else:
right=mid-1
实战
704.二分查找
https://leetcode.cn/problems/binary-search/
class Solution {
public:
int search(vector<int>& nums, int target) {
int n=nums.size();
int left=0;
int right=n-1;
int mid=(left+right)/2;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==target)
{
return mid;
}else if(nums[mid]>target)
{
right=mid-1;
}else{
left=mid+1;
}
}
return -1;
}
};
lower_ bound
在单调递增数组里
[10, 14, 19, 25, 27,31,33, 35, 42, 44]
查找第一个>=31的数(返回下标)
不存在返回 array.length
upper_ bound
在单调递增数组里
[10, 14, 19, 25, 27,31,33, 35, 42, 44]
查找第一个> 26的数(返回下标)
不存在返回array.length
lower_ bound和upper_ bound的问题是:
给定的target不一定在数组中存在
array[mid]即使不等于target,也可能就是最后的答案,不能随便排除在外
解决方案
根据个人喜好,掌握三种二分写法中的一种
- (1.1)+(1.2): 最严谨的划分,一侧包含,一侧不包含,终止于left == right
- (2): 双侧都不包含,用ans维护答案,终止于left > right
- (3):双侧都包含,终止于left+ 1 == right, 最后再检查答案
“90%的程序员都写不对二分”
所以必须熟记这三种之一
适用性更广的二分模板(1.1 -后继型)
查找lower_ bound (第一个>= target的数) ,不存在返回n
int 1eft = 0, right = n;
while (left < right) {
int mid = (left + right) 》1;
if (array[mid] >= target) // condition satisfied, should be included
right = mid;
else
1eft=mid+1;
}
return right ;
改为array[mid] > target 就是upper _bound
适用性更广的二分模板(1.2 -前驱型)
查找最后一个<= target的数,不存在返回-1
int left=-1,right=n-1;
while (1eft < right) {
int mid=(left+right+1)>>1;
if (array[mid] <= target) // condition satisfied, should be included
left = mid;
else
right = mid - 1;
}
return right;
适用性更广的二分模板(2)
也可以这样写
int left=0,right=n-1;
int ans = -1;
while (1eft <= right) {
int mid = (left + right) / 2;
if (array[mid] <= target) {
// update ans using mid
ans = max(ans, mid);
left=mid+1;
} else {
right = mid - 1;
}
}
适用性更广的二分模板(3)
还可以这样写
int left=0,right=n-1;
while (left + 1 < right) {
int mid = (1eft + right) 1 2;
if (array[mid] <= target)
left = mid;
else
right = mid;
}
//答案要么是left, 要么是right, 要么不存在
//此处检查left 和right, 返回一个合适的结果
实战
153.寻找旋转排序数组中的最小值
https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] <= nums[right]) {
right = mid;
}else {
left = mid + 1;
}
}
return nums[right];
}
};
154.寻找旋转排序数组中的最小值II
https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/
class Solution {
public:
int findMin(vector<int>& nums) {
int low = 0;
int high = nums.size() - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (nums[pivot] < nums[high]) {
high = pivot;
}
else if (nums[pivot] > nums[high]) {
low = pivot + 1;
}
else {
high -= 1;
}
}
return nums[low];
}
};
推荐使用1.1+1.2
只要“条件"单调,二分就适用
- 旋转排序数组中的最小值,序列本身并不单调
- 但“≤结尾"这个条件,把序列分成两半,一半不满足(>结尾) , 一半满足(≤结尾)
可以把“条件满足”看作1,不满足看作0,这就是一一个0/1分段函数,二分查找分界点
写出正确的二分代码“三步走”:
- 写出二分的条件(一般是一个不等式,例如upper_bound: > val的数中最小的)
- 把条件放到if…)里,并确定满足条件时要小的(right = mid)还是要大的(left = mid)
- 另一半放到else里(left= mid+ 1或right=mid- 1),如果是后者,求mid时补+1
如果题目有无解的情况,. 上界增加1或下界减小1,用于表示无解。
74.搜索二维矩阵
https://leetcode.cn/problems/search-a-2d-matrix/
class Solution {
public:
bool searchMatrix(vector<vector<int>> matrix, int target) {
auto row = upper_bound(matrix.begin(), matrix.end(), target, [](const int b, const vector<int> &a) {
return b < a[0];
});
if (row == matrix.begin()) {
return false;
}
--row;
return binary_search(row->begin(), row->end(), target);
}
};
69. x的平方根
https://leetcode.cn/problems/sqrtx/
class Solution {
public:
int mySqrt(int x) {
long left = 0, right = x;
while(left < right) {
long mid = (left + right + 1) / 2;
if(mid <= x / mid) {
left = mid;
}else {
right = mid - 1;
}
}
return right;
}
};
34.在排序数组中查找元素的第一个和最后一个位置
https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans;
int left = 0, right = nums.size();
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] >= target) {
right = mid;
}else {
left = mid + 1;
}
}
ans.push_back(right);
left = -1, right = nums.size() - 1;
while (left < right) {
int mid = (left + right + 1) / 2;
if(nums[mid] <= target) {
left = mid;
}else {
right = mid - 1;
}
}
ans.push_back(right);
if(ans[0] > ans[1]) return {-1, -1};
return ans;
}
};
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return vector<int> {-1, -1};
int left=leftRound(nums,target);
int right=rightRound(nums,target)-1;
if (left == nums.size() || nums[left] != target) {
return vector<int>{-1, -1};
}
return vector<int> {left,right};
}
int leftRound(vector<int>& nums,int target)
{
int left=0;
int right=nums.size();
int mid;
while(left<right)
{
mid=(left+right)/2;
if(nums[mid]>=target)
{
right=mid;
}else
{
left=mid+1;
}
}
return left;
}
int rightRound(vector<int>& nums,int target)
{
int left=0;
int right=nums.size();
int mid;
while(left<right)
{
mid=(left+right)/2;
if(nums[mid]>target)
{
right=mid;
}else
{
left=mid+1;
}
}
return left;
}
// 主函数
vector<int> searchRange1(vector<int>& nums, int target)
{
if (nums.empty()) return vector<int> {-1, -1};
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target) - 1; // 这里需要减1位
if (lower == nums.size() || nums[lower] != target)
{
return vector<int> {-1, -1};
}
return vector<int> {lower, upper};
}
// 辅函数
int lower_bound(vector<int> &nums, int target)
{
int l = 0, r = nums.size(), mid;
while (l < r)
{
mid = (l + r) / 2;
if (nums[mid] >= target)
{
r = mid;
}
else
{
l = mid + 1;
}
}
return l;
}
// 辅函数
int upper_bound(vector<int> &nums, int target)
{
int l = 0, r = nums.size(), mid;
while (l < r)
{
mid = (l + r) / 2;
if (nums[mid] > target)
{
r = mid;
}
else
{
l = mid + 1;
}
}
return l;
}
};
三分查找
三分法用于求单峰函数的极大值( 或单谷函数的极小值)
三分法也可用于求函数的局部极大/极小值
要求:函数是分段严格单调递增/递减的(不能出现一段平的情况)
以求单峰函数f的极大值为例,可在定义域[l,r]上取任意两点lmid, rmid
- 若f(lmid) <= f(rmid),则函数必然在Imid处单调递增,极值在[lmid, r]上
- 若 f(mid) > f(rmid),则函数必然在rmid处单调递减,极值在[l, rmid]上
lmid, rmid可取三等分点
也可取lmid为二等分点, rmid为lmid稍加一点偏移量
取黄金分割点最快*
实战
162.寻找峰值
https://leetcode.cn/problems/find-peak-element/
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while( left < right) {
int lmid = (left + right)/2;
int rmid = lmid + 1;
if( nums[rmid] <= nums[lmid] ){
right = rmid - 1;
}else {
left = lmid + 1;
}
}
return right;
}
};
374.猜数字大小
https://leetcode.cn/problems/guess-number-higher-or-lower/
class Solution {
public:
int guessNumber(int n) {
return (size_t)partition_point((bool*)1, (bool*)n, [] (const bool& i) {
return guess(size_t(&i)) == 1;
});
}
};
猜数游戏的规则如下:
- 每轮游戏,我都会从1到n随机选择一个数。
- 请你猜选出的是哪个数。
- 如果你猜错了, 我会告诉你,你猜测的数比我选出的数是大了还是小了。
二分答案最优性问题转化为判定问题的基本技巧
二分答案
对于一个最优化问题
求解:求一个最优解(最大值/最小值)
判定:给一个解,判断它是否合法(是否能够实现)
“判定”通常要比“求解”简单很多.
如果我们有了一个判定算法,那把解空间枚举+判定-遍,就得到解了
当解空间具有单调性时,就可以用二分代替枚举,利用二分+判定的方法快速求出最优解,这种方
法称为二分答案法
例如:求解-猜数;判定一 大了还是小了;
低效算法: 1到n挨个猜-遍;高效算法: 1 二分
实战
410.分割数组的最大值
https://leetcode.cn/problems/split-array-largest-sum/
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int left = 0, right = 0;
for(int num : nums) {
left = max(left, num);
right += num;
}
while(left < right) {
int mid = (left + right) / 2;
if(validate(nums, m, mid)) {
right = mid;
}else {
left = mid + 1;
}
}
return right;
}
private:
bool validate(vector<int> & nums,int m, int size) {
int box = 0;
int count = 1;
for(int num : nums) {
if(box + num <= size) {
box += num;
}else {
count++;
box = num;
}
}
return count <= m;
}
};
给定一个非负整数数组nums和一一个整数m,你需要将这个数组分成m个非空的连续子数组。
设计一个算法使得这m个子数组各自和的最大值最小。
求解:最小化"m个子数组各自和的最大值”
判定:给一个数值T,"m 个子数组各自和的最大值<= T”是否合法
换一一种说法:“ 能否将nums分成m个连续子数组,每组的和<= T”
为什么是"<=T" ?
nums= [7,2,5,10,8], -共有四种方法将nums分割为2个子数组
[7][2,5,10,8], sum=[7, 25], max = 25
[7,2][5,10,8], sum=[9, 23], max =23
[7,2,5][10,8], sum=[14, 18], max= 18
[7,2,5,10][8], sum=[24, 8], max=24
”能否将nums分成m个连续子数组,每组的和=T”一 只有18, 23, 24, 25合法
“能否将nums分成m个连续子数组,每组的和<= T”一17之 前不合法, 18之后合法
“能否将nums分成m个连续子数组,每组的和<= T”
这个解空间具有特殊的单调性 ---- 单调分段 0/1函数
直接求出18比较困难,但可以通过猜测一一个值T, 判断T是否合法(true or false),从而得知答
案是在T左侧还是右侧
最高效的猜测方法当然就是二分
通常用于最优化问题的求解
- 尤其是在出现“最大值最小”“最小值最大” 这类字眼的题目上
- “最大值最小”中的“最小”是一 个最优化目标,“最大” -般是一个限制条件(例如:限
制划分出的子数组的和)
对应的判定问题的条件通常是一个不等式
●不等式就反映了上述限制条件
关于这个条件的合法情况具有特殊单调性
此时就可以用二分答案把求解转化为判定的技巧
二分答案的本质是建立一个单调分段0/1函数
定义域为解空间(答案)值域为 0或1,
在这个函数上二分查找分界点
1482.制作m束花所需的最少天数
https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/
class Solution {
public:
int minDays(vector<int>& bloomDay, int m, int k) {
int latestBloom = 0;
for(int bloom : bloomDay) {
latestBloom = max(latestBloom, bloom);
}
int left = 0, right = latestBloom + 1;
while(left < right) {
int mid = (left + right) / 2;
if(validateOnDay(bloomDay, m, k, mid)){
right = mid;
}else{
left = mid + 1;
}
}
if(right == latestBloom + 1) return -1;
return right;
}
private:
bool validateOnDay(vector<int>& bloomDay, int m, int k, int now) {
int bouquet = 0;
int consecutive = 0;
for(int bloom : bloomDay) {
if(bloom <= now) {
consecutive++;
if(consecutive == k) {
bouquet++;
consecutive = 0;
}
}else{
consecutive = 0;
}
}
return bouquet >= m;
}
};
1011.在D天内送达包裹的能力
https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/
class Solution {
public:
int shipWithinDays(vector<int>& weights, int days) {
// 确定二分查找左右边界
int left = *max_element(weights.begin(), weights.end()), right = accumulate(weights.begin(), weights.end(), 0);
while (left < right) {
int mid = (left + right) / 2;
// need 为需要运送的天数
// cur 为当前这一天已经运送的包裹重量之和
int need = 1, cur = 0;
for (int weight: weights) {
if (cur + weight > mid) {
++need;
cur = 0;
}
cur += weight;
}
if (need <= days) {
right = mid;
}
else {
left = mid + 1;
}
}
return left;
}
};
911.在线选举
https://leetcode.cn/problems/online-election/
class TopVotedCandidate {
public:
vector<int> tops;
vector<int> times;
TopVotedCandidate(vector<int>& persons, vector<int>& times) {
unordered_map<int, int> voteCounts;
voteCounts[-1] = -1;
int top = -1;
for (auto & p : persons) {
voteCounts[p]++;
if (voteCounts[p] >= voteCounts[top]) {
top = p;
}
tops.emplace_back(top);
}
this->times = times;
}
int q(int t) {
int pos = upper_bound(times.begin(), times.end(), t) - times.begin() - 1;
return tops[pos];
}
};
875.爱吃香蕉的珂珂
https://leetcode.cn/problems/koko-eating-bananas/
class Solution {
public:
int minEatingSpeed(vector<int>& piles, int h) {
int low = 1;
int high = 0;
for (int pile : piles) {
high = max(high, pile);
}
int k = high;
while (low < high) {
int speed = (high - low) / 2 + low;
long time = getTime(piles, speed);
if (time <= h) {
k = speed;
high = speed;
} else {
low = speed + 1;
}
}
return k;
}
long getTime(const vector<int>& piles, int speed) {
long time = 0;
for (int pile : piles) {
int curTime = (pile + speed - 1) / speed;
time += curTime;
}
return time;
}
};
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习