二分枚举是二分查找的一种应用(这是我自己起的名字hhh,可别在外面说这是二分枚举的题),这类题相对于二分查找趣味性会更强一些,但是同时也需要更理解二分法的本质--枚举
题目链接
珂珂想用最慢的速度,在警卫回来之前吃掉所有的香蕉,数组中的每一个数对应每一堆香蕉的个数,我们需要返回的是最小的速度k 根/小时,如果pile[i]
我们根据输出进行反推,对于第一个例子,k=4 根/小时,对于第一堆香蕉有3根,所用的时间为3/4 向上取整为1小时,对于第二堆香蕉6需要使用2小时,第三堆香蕉7需要使用2小时,第四堆香蕉11需要使用3小时,而后3+2+2+1是不是刚好等于8小时
理解一下,以最慢的速度吃完香蕉,是不是有一点昨天二分法的题目中在所有可能正确的结果中寻找左侧边界的意思
为了使代码清晰可读,我们最好把函数和二分过程分开来写
piles为香蕉数组 h为最多用时 speed为我们返回的速度
findMinSpeed(piles,h)->speed:
#根据题意确定左右边界left和right
left,right=?,?
#二分法代码框架
while(lefth){
指针如何移动呢,思考一下
}else{
根据if内容写出else里的内容
}
}
#这里记得返回结果
这个函数的作用是计算出当前速度,以便查看最后的结果是否超时
sumSpeed(self,piles,speed)
#该怎么通过速度计算出最后用时呢,自己思考一下吧
羊羊可能不知道怎么两数相除向上取整,这里给出证明哦,对于x/y,例如2/3想让他等于1的写法是: (x+y-1)/y 这个要记住哦,以后很多时候都能用到的,证明如下:
x/y=A...B,其中A为除法结果结果,B为余数,其中0<=B
因此我们有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