题目描述
每堆香蕉 piles[i] 根,每小时最多吃一堆里 k 根(吃不完也得等下一小时)。要在 h 小时内吃完,求最小速度 k。
思路解析
最直接:速度 = 1、2、3… 挨个试,第一个「能在 h 小时内吃完」的就是答案。最坏要试到 max(piles) 那么大,香蕉堆里有上亿根就试上亿次。但这里藏着规律——速度越大,耗时只会越短、绝不会忽长忽短,这种单调性正是二分的信号。
转折:与其挨个试速度,不如在速度区间 [1, max(piles)] 上二分。先写个判定函数 hours(k):逐堆 ⌈piles[i]/k⌉ 求和,得到吃完要几小时。不变量:hours(k) 随 k 增大单调递减。对中间速度 mid,hours(mid) ≤ h 说明够快、还能更慢(收右);大于 h 说明太慢(收左)——照样每步砍一半。
速度区间 [1, 11]:把候选速度排成一排(1~11),左指针 l=速度1,右指针 r=速度11。注意:二分的对象是速度,不是香蕉堆;每个速度够不够快,靠判定函数 hours 算。
第 1 轮 · 取 mid:中间下标取速度 6。先别急着收缩,用判定函数算算速度 6 到底要几小时。
第 1 轮 · 判定(够快):速度 6:每堆向上取整算小时,1+1+2+2 = 6 小时 ≤ 8,够快!它是个可行解,但说不定还能更慢,所以保留它、往左找更小的。
第 1 轮 · 收缩:速度 7~11(更快但更浪费)整段灰掉丢弃,r 收到 mid(不减 1,速度6 自己可能就是答案)。范围缩到速度 [1, 6]。
第 2 轮 · 取 mid:新范围 [1, 6],中间取速度 3。继续用判定函数算它要几小时。
第 2 轮 · 判定(太慢·负例):速度 3:1+2+3+4 = 10 小时,大于 8,超时了,不可行。说明速度 3 以及比它更慢的都不行,得加速——这一半要丢掉。
第 2 轮 · 收缩:速度 1、2、3(含 mid)都太慢,整段灰掉,l 跳到 mid+1(速度4)。范围缩到速度 [4, 6]。
第 3 轮 · 取 mid 并判定:范围 [4, 6],中间速度 5:1+2+2+3 = 8 小时,正好 ≤ 8,够快。保留它往左找,r 收到 mid(速度5)。范围缩到速度 [4, 5]。
第 4 轮 · 取 mid 并判定:范围 [4, 5],中间速度 4:也是 8 小时 ≤ 8,够快。r 收到 mid(速度4),此时 l == r,区间只剩速度 4,循环结束。
收敛 · 答案:l 和 r 撞在速度 4——这就是能在 8 小时内吃完的最小速度。再慢一档(速度3)就超时。
「求最小/最大的、满足某条件的值」,且条件随值单调,就能二分答案:x 的平方根、D 天送货都是它。
参考代码(Python)
1import math
2def minEatingSpeed(self, piles, h):
3 def hours(k): # 判定函数:速度 k 要几小时
4 return sum(math.ceil(p / k) for p in piles)
5 l, r = 1, max(piles) # 在速度区间上二分
6 while l < r: # l==r 时即答案
7 mid = (l + r) // 2
8 if hours(mid) <= h:
9 r = mid # 够快,收右试更小
10 else:
11 l = mid + 1 # 太慢,加速
12 return l复杂度分析
- 时间复杂度:O(n log max) —— max=max(piles);二分 log(max) 次,每次判定 hours 扫一遍 n 堆
- 空间复杂度:O(1) —— 只用 l、r、mid 几个变量,hours 现算不存表
套路模板
记住骨架:二分的是答案区间、核心是写对 check、合法时收右求最小。求最大值就把收缩方向反过来。
1# 求「满足条件的最小值」、且条件随值单调时套用
2def check(x): ... # 判定:速度/容量 x 行不行
3l, r = 最小可能, 最大可能
4while l < r:
5 mid = (l + r) // 2
6 if check(mid): r = mid # 行,收右求更小
7 else: l = mid + 1 # 不行,加大
8return l易错点
- 错误写法:在 piles 上二分、上界乱填 → 正确写法:在速度区间 [1, max(piles)] 上二分(二分的对象是答案(速度),不是数组;上界必须取 max(piles),因为最快也只需每小时吃完最大那堆,再大没意义)
- 错误写法:hours(mid) <= h 时写 l = mid+1 → 正确写法:此时 r = mid(往更小速度收)(我们要的是满足条件的最小值,够快时应保留 mid 并往左找,方向写反会得到偏大答案)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。