算法小抄7-二分枚举

文章介绍了如何运用二分枚举解决珂珂吃香蕉的问题,强调理解二分法的本质——枚举,并提供了详细的解题思路和代码框架。此外,还提及了一个类似题目‘在D天送达包裹的能力’,探讨了利用二分法寻找数组分割的最优解,指出了解题的关键在于探索问题的单调性。
摘要由CSDN通过智能技术生成

二分枚举是二分查找的一种应用(这是我自己起的名字hhh,可别在外面说这是二分枚举的题),这类题相对于二分查找趣味性会更强一些,但是同时也需要更理解二分法的本质--枚举

爱吃香蕉的珂珂

题目链接

题目大意

珂珂想用最慢的速度,在警卫回来之前吃掉所有的香蕉,数组中的每一个数对应每一堆香蕉的个数,我们需要返回的是最小的速度k 根/小时,如果pile[i]<k,吃完香蕉的时间会进行向上取整

我们根据输出进行反推,对于第一个例子,k=4 根/小时,对于第一堆香蕉有3根,所用的时间为3/4 向上取整为1小时,对于第二堆香蕉6需要使用2小时,第三堆香蕉7需要使用2小时,第四堆香蕉11需要使用3小时,而后3+2+2+1是不是刚好等于8小时

理解一下,以最慢的速度吃完香蕉,是不是有一点昨天二分法的题目中在所有可能正确的结果中寻找左侧边界的意思

题目思路

  • 对于有规律或者有序的题目条件才能使用二分法,珂珂吃香蕉的速度越快,所消耗的时间越少,反之速度越慢,所消耗的时间越多,这就是此题速度条件的单调性
  • 由于珂珂一小时内只能选择一堆香蕉吃,因此每堆香蕉吃完的耗时=香蕉数量/珂珂一小时吃香蕉的数量,根据题意这里要向上取整
  • 题目要我们返回速度,那我们就直接使用二分法枚举所有的速度,返回最左侧边界的那一个复合题目条件的速度
  • 首先要确定二分法搜索的范围,最小的速度是1,因为珂珂必须吃香蕉,最大的速度为数组中最大香蕉的个数,这是因为根据题意,每一堆香蕉至少要吃一个小时,速度再超过最大香蕉的个数就已经没有意义了,对于第一个例子[3,6,7,11],我们最大的速度为11根/h,要吃4个小时,因此我们二分枚举的范围在[1,4]之中

代码框架

为了使代码清晰可读,我们最好把函数和二分过程分开来写
piles为香蕉数组 h为最多用时 speed为我们返回的速度

findMinSpeed(piles,h)->speed:
   #根据题意确定左右边界left和right
   left,right=?,?
   #二分法代码框架
   while(left<right){
        mid=left+(right-left)//2 #取得中点
        #这里填写一定不符合要求的条件,即大于指定的时间
        if(sumSpeed(self,piles,mid)>h){
            指针如何移动呢,思考一下
        }else{
            根据if内容写出else里的内容
        }
   }
   #这里记得返回结果
    
这个函数的作用是计算出当前速度,以便查看最后的结果是否超时
sumSpeed(self,piles,speed)
  #该怎么通过速度计算出最后用时呢,自己思考一下吧

羊羊可能不知道怎么两数相除向上取整,这里给出证明哦,对于x/y,例如2/3想让他等于1的写法是: (x+y-1)/y 这个要记住哦,以后很多时候都能用到的,证明如下:

x/y=A...B,其中A为除法结果结果,B为余数,其中0<=B<y

因此我们有x=A*y+B,带入(x+y-1)/y这个式子

则有(x+y-1)/y=A+1+(B-1)/y=A+1...(B-1)/y,后面的部分是余数会被计算机自动舍去,A+1就是我们最后的向上取整需要得到的结果

羊羊写出来了吗,来对一下答案吧

分割线---------------------------------------------------------------------------------------------------------------

class Solution:
    def minEatingSpeed(self, piles, h: int) -> int:
        maxVal=max(piles)
        left = 1
        right = maxVal
        while left < right:
            speed = left+(right-left)//2
            if Solution.sumSpeed(self,piles,speed) > h:
                left = speed+1
            else:
                right = speed
        return left


    def sumSpeed(self,piles,speed):
        s=0
        for pile in piles:
            s+=(pile+speed-1)//speed
        return s

小结一下:

  • 题目要求什么,就是我们二分法需要枚举得到的结果
  • sumSpeed函数作用于if条件中,用于判断一定不符合条件的情况,在这里我们计算吃香蕉所用的时间
  • 向上取整还记得吗(x+y-1)/y

类似的题目还有很多,相信羊羊做完这题就都能秒杀了,题目链接放在下面哦

在D天送达包裹的能力

x的平方根

分割数组的最大值

分割数组的最大值这道题比价难,在这里我写一点思路:

题意: 我们需要挑选一种分割方式,将nums数组分割成m个非空的连续子数组使得他们加起来的和是最小的

探索题目的单调性: 如果定义"数组各自和的最大值"很大,导致的结果是分割数很小,反之"数组各自和的最大值"很小,那么导致分割数很大,当我们搜索到了"数组各自和的最大值"恰好使得分割数为m,此时我们不应该放弃搜索,而应该继续收缩边界

举个例子理解一下"数组各自和的最大值"这个概念,以下统称为s:

  • 对于用例[7,2,5,10,8] m=2
  • 如果定义s为20,此时分割为[7,2,5] [10,8],没有达到临界值18,m=2
  • s为19,18这样的分割条件依然不变
  • 当s定义为17时,[10,8]已经不满足我们的条件了,因为10+8>17,所以需要继续分割,这样的分割就会导致分割变为[7,2,5] [10] [8] m=3,此时是不是就越过了我们的边界点
  • 而m变成3之前的值18就是我们需要返回的结果

因为是困难题,羊羊可以先看代码,再去试着自己把整个代码敲出来:

class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:
        """第一步定义二分的范围"""
        max_nums = max(nums)  # nums的最大值
        sum_nums = sum(nums)  # nums的和
        left = max_nums
        right = sum_nums

        """第二步定义条件函数"""
        # "最大的子数组和"用maxSubArraySum表示,它的取值范围是[max_nums, sum_nums]
        # 当nums分割成len(nums)个子数组时,maxSubArraySum == max_nums
        # 当nums分割成1个子数组时,maxSubArraySum == sum_nums
        # 现在把问题反过来求,不求分割成m个子数组对应的maxSubArraySum,我们来求当
        # maxSubArraySum等于x时数组被分割成了多少个子数组,x的范围就是[max_nums, sum_nums]

        # 当maxSubArraySum==x(某个值)时,如果对应的子数组数目大于m,
        # 那就说明x的值小了呀(maxSubArraySum的值越小,nums被分的越细,子数组越多),
        # 所以我们应该去[x+1, sum_nums]中去找我们需要的x,相反如果对应的子数组数目小于m,
        # 那就说明x的值太大了(maxSubArraySum的值越大,nums被分的越粗,子数组越少),
        # 所以我们应该去[max_nums, x-1]中去找我们需要的x
        # 综上所述用二分法是可行滴,x每次取mid就行了

        # 最后一个问题,如果求maxSubArraySum==x时对应的子数组数目
        def subArrayNum(maxSubArraySum):
            """
            return: 返回maxSubArraySum对应的子数组数目
            """
            subArrayNum = 1  # 子数组数目默认是1(因为最后一个子数组必然不执行if,所以要加1)
            subArraySum = 0  # 记录当前子数组的和(当前子数组里还没有元素呢,接下来给它添加元素))
            for num in nums:
                if subArraySum + num > maxSubArraySum:
                    # 子数组是从左往右连续切割的,当subArrayNum + num大于maxSubArraySum时
                    # num肯定不能进当前子数组的,因为每个子数组和必须不超过maxSubArraySum
                    subArrayNum += 1  # 从num的前面切割,子数组数目加1
                    subArraySum = num  # num此时已经属于下一个子数组了,只是还没切割
                else:
                    subArraySum += num  # 一直给当前子数组添加num,直到它满足if进去切割
            return subArrayNum

        """第三步补充二分法"""
        while left < right:
            mid = (left + right) // 2
            sas = subArrayNum(mid)  # mid对应的子数组数目
            if sas > m:  # mid值小了
                left = mid + 1
            else:
                right = mid
        return left

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值