爱吃香蕉的珂珂 图解题解
想找最小可行速度,不用一档一档试——速度和耗时的单调关系,让二分直接锁定答案。
就像评估快递员每小时最多派几单才能按时派完——你不用从「每小时1单」一路试到「每小时全部单量」,因为速度越快耗时只会越短,这种单调性是二分的信号。在速度区间 [1, max] 上取中间值,写个判定函数算「这个速度要几小时」:够快就往左找更慢的可行解,太慢就往右提速——每次砍一半,直到逼出最小速度。
这道题到底在问什么
- 输入
- piles=[3,6,7,11], h=8
- 输出
- 4
先想最直接的笨办法
l 和 r 撞到一起,这一格就是答案:最小可行速度 4。它能在 8 小时内吃完(8 小时),再慢一档(速度 3)就要 10 小时、超时。对比暴力要试到第几个,二分只试了 4 次。(动画第 23 步)
最优解:为什么这么做
一句话答案:LeetCode 875 爱吃香蕉的珂珂用二分答案:吃速越大耗时越短这条单调性,让判定函数 hours(k)=各堆向上取整之和能二分找最小可行速度。时间 O(n·log(max))、空间 O(1)。
珂珂每小时只吃一堆,这道题到底在求哪个速度
给几堆香蕉 piles 和警卫回来前的小时数 h,珂珂每小时挑一堆、以固定速度 k 根吃,一堆吃光了剩下时间也不换堆。求能在 h 小时内吃完全部香蕉的最小整数速度 k。题面 piles=[3,6,7,11]、h=8 时答案是 4:速度 4 刚好 8 小时吃完,再慢一档就超时。
为什么从 1 到最大堆一个个试速度会白扫一大截
速度最小取 1、最大取 11(最大那堆 11 根,速度 11 一小时吃光,再快没意义)。一个笨办法是从 1 起逐个速度试,每试一个就把四堆各要几小时加起来、看超不超 h。可最坏要一路试到最大那档才停,扫过的速度数和这段速度范围(1 到最大堆)一样多,时间 O(n·max);范围大到 10⁹ 就挨个试不动了。
速度和「来不来得及」之间,藏着一条能二分的分界线
把每个速度的总小时数排一排会看出:速度越大,吃完的总时间只会越短、越来得及——这就是单调(沿一个方向走、不回头)。于是所有速度被切成两半,左边都太慢、来不及,右边都够快、来得及,答案正是界线上第一个够快的速度。有了单调分界,就能用二分查找逼近它,不必挨个试。这里点破一句:二分的不是某个数组下标,而是所有可能的吃速。这种不直接推公式、而是猜一个速度再验证行不行的思路,叫二分答案。
判定函数怎么写,够快和太慢又各往哪半收
先把「行不行」做成判定函数(输入一个速度、回答够不够快的小函数)hours(k):每堆各要 ⌈香蕉数 ÷ k⌉ 小时(⌈⌉ 是向上取整,吃不满一小时也算一整小时),加起来就是这速度的总耗时。二分在区间 [lo, hi] 取中点 mid 算 hours(mid):≤ h 够快,把 hi 收到 mid(mid 可能正是最小,得留);> h 太慢,mid 连左边全不行,把 lo 跳到 mid+1。缩到 lo 和 hi 撞上,那格就是最小可行速度。
拿题面 piles、h=8 逐轮列出 mid 和判定
起点 lo=1、hi=11。第一轮 mid=(1+11)//2=6,hours(6)=1+1+2+2=6 ≤ 8 够快,hi 收到 6。第二轮 lo=1、hi=6,mid=3,hours(3)=1+2+3+4=10 > 8 太慢,lo 跳到 4。第三轮 lo=4、hi=6,mid=5,hours(5)=1+2+2+3=8 ≤ 8 够快,hi 收到 5。第四轮 lo=4、hi=5,mid=4,hours(4)=1+2+2+3=8 ≤ 8 够快,hi 收到 4。此时 lo=hi=4,循环停,返回 4。
lo 起点搁 0,判定里的除法当场崩
二分把速度档数压到约 log max,每档 hours() 扫 n 堆,时间 O(n·log max)、空间 O(1)。lo 起点得从 1 起,写成 0 时 ⌈p ÷ 0⌉ 当场除零崩掉。够快时收缩只能写 hi=mid,写成 hi=mid-1 会把可能就是答案的 mid 丢出区间,返回值偏大一档。太慢那支要写 lo=mid+1,若写成 lo=mid,等 lo、hi 只差一格时 mid 永远卡在 lo、区间缩不动,就死循环。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这条「够快就试更慢、太慢就加速」,下面每一帧都在套它。
- 4速度最小取 1,最大取 11(最大那堆 11 根,速度 11 时一小时吃光,再快没意义)。l、r 框住当前要搜的速度区间。
- 5速度 1:27 小时 > 8,来不及,cur 往右挪一格试更快的。逐个扫最坏要试到 11——下面用二分把它压到 log 次。
- 6速度 2:15 小时 > 8,来不及,cur 往右挪一格试更快的。逐个扫最坏要试到 11——下面用二分把它压到 log 次。
- 7速度 3:10 小时 > 8,来不及,cur 往右挪一格试更快的。逐个扫最坏要试到 11——下面用二分把它压到 log 次。
- 8速度 4:8 小时 ≤ 8,来得及。注意从这格往右每个速度都来得及(速度越快越省时)——左边来不及、右边来得及,是一条单调分界线。逐个扫最坏要试 11 次,太慢。
- 9速度 5:8 小时 ≤ 8,来得及。注意从这格往右每个速度都来得及(速度越快越省时)——左边来不及、右边来得及,是一条单调分界线。逐个扫最坏要试 11 次,太慢。
- 10速度 6:6 小时 ≤ 8,来得及。注意从这格往右每个速度都来得及(速度越快越省时)——左边来不及、右边来得及,是一条单调分界线。逐个扫最坏要试 11 次,太慢。
- 11在区间 [1..11] 取中点速度 6:每堆各算 ⌈香蕉数 / 6⌉ 小时,合计 6 小时。
- 12够快(6 ≤ 8)。既然 6 都来得及,比它更快的速度只会更快、更不缺这道答案的「最小」要求——右半边整段排除。但 6 自己可能正是最小答案,要留着。
- 13因为够快,r 收到 mid(保留 6 这个候选),继续在更慢一侧找有没有更小的可行速度。区间从 11 格砍到 6 格。
- 14在区间 [1..6] 取中点速度 3:每堆各算 ⌈香蕉数 / 3⌉ 小时,合计 10 小时。
- 15太慢(10 > 8)。3 都吃不完,比它还慢的更不行——左半边连同 mid 一起排除,只能去更快的一侧。
- 16因为太慢,l 跳到 mid 右边一格(4),把不可行的全甩掉,继续在更快一侧找。区间从 6 格砍到 3 格。
- 17在区间 [4..6] 取中点速度 5:每堆各算 ⌈香蕉数 / 5⌉ 小时,合计 8 小时。
- 18够快(8 ≤ 8)。既然 5 都来得及,比它更快的速度只会更快、更不缺这道答案的「最小」要求——右半边整段排除。但 5 自己可能正是最小答案,要留着。
- 19因为够快,r 收到 mid(保留 5 这个候选),继续在更慢一侧找有没有更小的可行速度。区间从 3 格砍到 2 格。
- 20在区间 [4..5] 取中点速度 4:每堆各算 ⌈香蕉数 / 4⌉ 小时,合计 8 小时。
- 21够快(8 ≤ 8)。既然 4 都来得及,比它更快的速度只会更快、更不缺这道答案的「最小」要求——右半边整段排除。但 4 自己可能正是最小答案,要留着。
- 22因为够快,r 收到 mid(保留 4 这个候选),继续在更慢一侧找有没有更小的可行速度。区间从 2 格砍到 1 格。
- 23l 和 r 撞到一起,这一格就是答案:最小可行速度 4。它能在 8 小时内吃完(8 小时),再慢一档(速度 3)就要 10 小时、超时。对比暴力要试到第几个,二分只试了 4 次。
⚠️ 容易写错的地方
✗ 错:小时数用整除 p/k
✓ 对:必须向上取整 ⌈p/k⌉
一堆没吃完也占满 1 小时,下取整会少算时间
✗ 错:够快时写 lo=mid
✓ 对:够快应 hi=mid(保留 mid 这个候选)
mid 自己可能就是答案,不能跳过它
✗ 错:hi 初值取成 piles 之和
✓ 对:hi=max(piles) 足矣
速度等于最大堆时一小时就能吃光任意一堆,再大无意义
完整代码(Python / C++ / Java)
Python
import math
def minEatingSpeed(piles, h):
def hours(k): # 速度 k 吃完要几小时
return sum(math.ceil(p / k) for p in piles)
lo, hi = 1, max(piles) # 速度区间 [1, max]
while lo < hi:
mid = (lo + hi) // 2
if hours(mid) <= h: # 够快
hi = mid # 试更慢
else: # 太慢
lo = mid + 1 # 加速
return lo # 最小可行速度C++
int minEatingSpeed(vector<int>& piles, int h){
auto hours = [&](long long k){
long long t = 0;
for(int p : piles) t += (p + k - 1) / k; // 向上取整
return t;
};
int lo = 1, hi = *max_element(piles.begin(), piles.end());
while(lo < hi){
int mid = lo + (hi - lo) / 2;
if(hours(mid) <= h) hi = mid; // 够快, 试更慢
else lo = mid + 1; // 太慢, 加速
}
return lo;
}Java
public int minEatingSpeed(int[] piles, int h) {
int lo = 1, hi = 0;
for (int p : piles) hi = Math.max(hi, p); // 速度上界 = 最大堆
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (hours(piles, mid) <= h) { // 够快
hi = mid; // 试更慢
} else { // 太慢
lo = mid + 1; // 加速
}
}
return lo; // 最小可行速度
}
private long hours(int[] piles, int k) {
long t = 0;
for (int p : piles) t += (p + k - 1) / k; // 向上取整
return t;
}复杂度
时间
O(n·log(max))
二分速度 log(max) 轮,每轮 hours() 扫一遍 n 堆
空间
O(1)
只用 lo/hi/mid 几个变量,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 爱吃香蕉的珂珂 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这道题怎么一眼看出该用二分答案,而不是别的解法?+
识别信号是「求满足某条件的最小或最大值」加上「这个值随某个量单调变化」。本题要最小速度 k,而速度越大越省时、够快的速度连成右半段、太慢的连成左半段,是一条单调分界。凡是答案本身单调、又能写出一个判定函数验证某个候选行不行,就能把「直接算答案」换成「二分猜答案 + 判定验证」。运输能力、分割数组最大值这类题都是同一套路。
为什么二分的是速度,而不是香蕉数或小时数?+
二分要架在一条单调轴上,而这题里随速度单调变化的量是总耗时——速度大一档,总小时数只减不增。香蕉数是固定输入、不是可调的候选;小时数 h 是给定的约束、当判定的阈值,也不是要搜的量。真正要搜的是「最小可行速度」,所以把速度当作二分区间 [1, max(piles)],用 hours(k) ≤ h 当判定条件。
判定函数里为什么用向上取整,不是普通除法?+
珂珂一小时最多吃 k 根,且吃光当前堆就停、不接着吃下一堆。所以一堆 p 根、速度 k,要 ⌈p ÷ k⌉ 小时:比如 7 根、速度 4,第一小时吃 4 根、第二小时吃剩下的 3 根,第二小时没吃满也算一整小时,正好是 ⌈7 ÷ 4⌉=2。写成整除 7÷4=1 会漏掉那个零头小时,把总时间算少,反而误判一个太慢的速度够快。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 爱吃香蕉的珂珂 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。