根据labuladong算法小抄里东哥的总结。有一系列的问题都可以用二分搜索的泛化来解决。
首先需要从题目中抽象出一个自变量x,一个关于x的函数f,以及一个目标值target
同时这三个元素得满足以下条件
(1)f必须是x上的单调函数,单调递增或者单调递减都可以
(2)题目是让你计算满足约束条件的f(x)==target时的x的值。
下面以例题来帮助理解加深这一思路。
一、爱吃香蕉的珂珂 875
1、分析
可以看到该题是满足我们开头说的条件的。自变量就是吃香蕉的速度k,然后写一个以k为自变量,然后吃完香蕉所需要的时间hour为因变量的函数f。然后就可以用二分搜索的方法取找最小k,也就是我们要找的是左边界。
其中需要注意的一点是,自变量的左右边界也是需要特别注意的。这里她最少每小时吃1根,最多可以一小时就吃完数量最多的那些香蕉。这样left和right就定了。接下来常规寻找左边界套路即可。
具体见代码。
2、代码
python
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
# 确定自变量的最值,最小值是多少,最大值是多少(这里是一小时吃香蕉的根数,最少吃一根,最多就是这堆香蕉中的数量最大值)
left,right = 1,max(piles)
# 辅助函数,是跟自变量相关的单调函数
def fn(x,piles):
hour = 0
for p in piles:
hour += p//x
if p%x!=0:
hour += 1
return hour
while left<=right:
mid = left+(right-left)//2
# 考虑我们到底要找的是左边界还是右边界
if fn(mid,piles)==h:
right = mid-1
elif fn(mid,piles)<h:
right = mid-1 #考虑,怎样让fn的值变大一点
elif fn(mid,piles)>h:
left = mid+1 #考虑,怎样让fn的值变小一点
return left
js
/**
* @param {number[]} piles
* @param {number} h
* @return {number}
*/
var fn = function(x,piles){
let hour = 0;
for(let p of piles){
hour += Math.floor(p/x);
if(p%x!==0){
hour += 1;
}
}
return hour;
}
var minEatingSpeed = function(piles, h) {
// 注意到js求数组最大值,以及底板除的方法
let left=1,right=Math.max(...piles);
while(left<=right){
let mid = left+Math.floor((right-left)/2);
if(fn(mid,piles)==h){
right = mid-1;
}else if(fn(mid,piles)>h){
left = mid+1;
}else if(fn(mid,piles)<h){
right = mid-1;
}
}
return left;
};
二、在D天内送达包裹的能力 1011
1、分析
首先还是按照要求抽象出三个要素,然后找到自变量的范围。由于包裹是不能拆掉的,所以left值应该是所有weights元素中的最大值。right则是weights数组求和,即一天就运完所有包裹。
2、代码
class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
left,right = max(weights),sum(weights) #注意到选择边界也是需要特别注意的,需要与题目要求挂钩。例如包裹是不能分开装的,所以最小运载能力应该等于weights数组最大值,而最大运载能力则是满足一条运完等于所有weight的和
def fn(x,weights):
day,temp = 0,0
for w in weights:
temp += w
if temp>x:
day += 1
temp = w
return day+1 #注意到,如果加上最后一个包裹如果触发了day+1条件,则需要对最后这个包裹进行单独运输;如果加上最后一个包裹也没有触发day+1条件,那么需要对最后这一批包裹进行单独运输。所以最后返回需要+1
while left<=right:
mid = left+(right-left)//2
if fn(mid,weights)==days:
right = mid-1 #因为我们找最低运载能力,所以是找左边界!
elif fn(mid,weights)<days:
right = mid-1
elif fn(mid,weights)>days:
left = mid+1
return left
js
/**
* @param {number[]} weights
* @param {number} days
* @return {number}
*/
var fn = function(x,weights){
let day=0,temp=0;
for(let w of weights){
temp += w;
if(temp>x){
day += 1;
temp = w;
}
}
return day+1;
}
var shipWithinDays = function(weights, days) {
function sum(arr){
return arr.reduce((pre,cur)=>{
return pre+cur
})
}
let left=Math.max(...weights),right=sum(weights);
while(left<=right){
let mid = left+Math.floor((right-left)/2);
let need_ = fn(mid,weights);
if(need_==days){
right = mid-1;
}else if(need_<days){
right = mid-1;
}else if(need_>days){
left = mid+1;
}
}
return left;
};
三、分割数组的最大值
1、分析
其实这道题和上一道运输题是一样的。我觉得需要绕一下的就是二分搜索的边界
因为至少是一个元素为一个子数组,所以left值应该是nums中元素的最大值。而right则是将整个数组当作一个子数组,然后求和。
具体见代码
2、代码
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
left,right = max(nums),sum(nums) #注意到子数组的和的最大值,那么这个值的最小情况:每个元素单独成一个子数组时,取所有元素中的最大值;最大的情况:原数组本身作为一个子数组,所以为所有元素之和。
def fn(x,nums): #找到子数组最大值和子数组数量之间的关系
res,cur = 0,0
for n in nums:
cur += n
if cur>x: #每当元素之和加起来大于x了,就证明得另分为一个子数组了,所以res+1
res += 1
cur = n
return res+1 #这题和船运1011题很像,也就是剩下的元素会单独构成一个子数组的
while left<=right:
mid = left+(right-left)//2
need_ = fn(mid,nums)
if need_==m:
right = mid-1 #明确到我们要找的是左边界
elif need_<m:
right = mid-1
elif need_>m:
left = mid+1
return left
js
/**
* @param {number[]} nums
* @param {number} m
* @return {number}
*/
var fn = function(x,nums){
let res=0,cur=0;
for(let n of nums){
cur += n;
if(cur>x){
res += 1;
cur = n;
}
}
return res+1
}
var splitArray = function(nums, m) {
function sum(arr){
return arr.reduce((pre,cur)=>{
return pre+cur;
})
}
let left=Math.max(...nums),right=sum(nums);
while(left<=right){
let mid = left+Math.floor((right-left)/2);
let need_ = fn(mid,nums);
if(need_==m){
right = mid-1;
}else if(need_<m){
right = mid-1;
}else if(need_>m){
left = mid+1;
}
}
return left;
};