【算法训练】二分搜索的拓展应用

361 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

根据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;
};