news 2026/9/8 4:00:45

LeetCode 875 爱吃香蕉的狒狒:二分答案模型与面试思路解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 875 爱吃香蕉的狒狒:二分答案模型与面试思路解析

1. 从一道二分查找题聊聊我的刷题节奏

day73(2.1)——leetcode面试经典150,这个标题我自己回看都觉得像某种密码,其实就是一个最普通的刷题打卡记录:第73天,2月第1题,用的题单是LeetCode官方整理的面试经典150。熟悉我的朋友都知道,我很少追着热门题单满屏跑,反而更愿意在经典题上反复磨,因为面试考的不是你会不会背题解,而是你面对一道从没见过的题时,能不能把见过的模型套上去。

今天刷到的是二分查找专题里的经典题:爱吃香蕉的狒狒,LeetCode 875题。很多人搜“073爱吃香蕉的狒狒”,其实就是把打卡天数当成题号搜错了位置,这道题的真实编号是875,中文站通常叫“爱吃香蕉的狒狒”,英文站叫Koko Eating Bananas。它属于面试经典150里二分查找部分的必刷题,也是我眼里最典型的“二分答案”模型题——题目表面上是一个模拟过程,实际上考察的是你有没有能力把“最优化问题”转化成“判定问题”,再用二分去逼近答案。

这篇不是单纯贴一份能AC的代码,我想把这道题从读题、建模、边界处理、面试表达,到它背后的一整类题型,都掰开揉碎讲清楚。如果你正在准备春招、秋招或者社招跳槽,刷了几天题总觉得原地踏步,那这篇文章应该能帮你省下不少时间。

先交代一下我自己的刷题进度,方便你对照参考。我在第1到第30天左右,主攻线性表、哈希表、字符串这些基础结构;第31到第60天,集中啃树、图、搜索和动态规划模板;从第61天开始进入二轮强化,每天不再追求“刷了多少道”,而是要求自己“讲得清楚一道题的来龙去脉”。day73正好落在二轮的二分查找专项上,2.1的意思就是2月第一篇复盘日志,恰好用它来做二分答案模型的突破口。

2. 为什么选面试经典150而不是无脑刷热门100

2.1 经典150与热门100的核心差异

很多人打开LeetCode就开始做热门100,这个思路不能说错,但如果你认真盯着榜单看,会发现热门100更像是“全网点击量最高的题”,覆盖面广、难度跨度大,适合用来查漏补缺或者临阵磨枪。而面试经典150是官方按数据结构和算法专题组织的一套题单,每个专题下面都有若干道代表性题目,从链表、二叉树、图论到二分、滑动窗口、动态规划,基本能把你面试中大概率遇到的套路都过一遍。

我自己在day60之前几乎只刷经典150,到了冲刺阶段才用热门100做随机抽练。原因很简单:经典150像一本按章节排好的教材,热门100像一本乱序词表。教材用来搭建知识框架,词表用来最后自测语感,两者不是互相替代关系。面试经典150和热门100之间大概有三成题目重叠,但这不影响你的任何选择。至于周赛题,我一般只在周日练手感,平时不会把周赛难题当作主刷题单,原因后面细说。

2.2 73天下来我实际执行的刷题节奏

如果你也打算用两个月左右刷完这150道题,我建议你把计划拆成三个周期。第一个周期,按专题过一遍基础题,目标是“看得懂题解、能独立写出模板”,耗时大约30天。第二个周期,开始做跨专题的中等题,同一道题尽量尝试两种以上解法,目的是“把模板用熟”,耗时大约20天。第三个周期,重点做高频难题和错题回顾,并且每隔几天用一场限时模拟来检验速度,耗时15到20天。

day73 这个阶段我在第二周期和第三周期之间。二轮复习的题目不能只是“AC一下就行”,我会在每道题后面补一段自己的讲解稿,假想对面坐着面试官,把算法流程、复杂度、边界条件全部口头讲一遍。很多代码写得出的同学,一到说话就卡壳,就是缺乏这个“出声训练”的环节。刷题打卡如果只记录“做了多少道”而不记录“我踩了什么坑、怎么讲清楚”,那数字增长的意义会大打折扣。

2.3 打卡记录应该记什么才不白费

我的每日复盘模板分为四块:核心考点、解法流程、边界条件、面试延伸。以今天这道875为例,核心考点是二分答案;解法流程是“确定搜索区间,判断当前速度是否能在H小时内吃完”;边界条件是左边界从1开始、右边界取堆的最大值、判断时向上取整;面试延伸则是“单调性如何证明”“如果H小于堆数量怎么办”。这四块内容比代码本身重要得多。

很多人的笔记只是把题解复制一遍,这没有意义。真正有价值的记录是你自己当时的错误思路:比如我第一次做的时候就想着直接模拟,从速度1开始慢慢加到能吃完为止,结果在测试用例上跑了超时才意识到要二分。这个“为什么没想到二分”的过程,比最终代码更值得写进打卡里。

3. 爱吃香蕉的狒狒到底在考什么

3.1 先还原题目,别被故事带偏

原题讲的是狒狒(也有人翻译成珂珂)有一堆香蕉,每堆香蕉数量记录在数组 piles 里,它每小时最多吃一堆,如果这一堆少于它当前的速度 k,那么这一小时只吃这一堆,吃不完也不吃另一堆。现在要求它必须在 H 小时内吃完所有香蕉,问最小的速度 k 是多少。

故事绕来绕去,翻译成程序员能懂的话就是:给定一个数组 piles 和一个总时间限制 H,我们选择一个速度 k,对于数组里每一个元素 p,吃掉它需要 ceil(p / k) 小时,最终总耗时不能超过 H。我们要找满足条件的最小的 k。这里的 ceil 是向上取整,因为哪怕只剩下1根香蕉,也要花一整小时去吃完这一堆,不能去开下一堆。

这个“每小时最多吃一堆”的设定是题目的灵魂,它决定了同样的速度在不同分布下耗时是不同的。比如速度是4时,7根一堆需要2小时而不是1.75小时,因为一小时是离散的。

3.2 暴力模拟为什么不行

最容易想到的解法是枚举速度,从1一直试到数组最大值。对于每个速度,遍历每一堆累加时间,检查是否小于等于 H。这个方法逻辑完全正确,但复杂度是 O(max(piles) * n),如果 piles 里出现 10 的 9 次方级别的数字,枚举就要循环十亿次,配合上万长度的数组必然超时。

这就是大多数二分答案题的共性标志:你要求一个满足条件的最小值,而且这个条件的判定函数是单调的。速度越快,总耗时越短,不可能出现速度更快反而耗时更长的情况。既然判定结果随速度变化呈现“前面不行,后面行”的阶梯状,我们就可以用二分查找快速找到拐点位置。

3.3 单调性才是二分的入场券

为什么说速度越快总耗时一定不会增加?因为对任意一堆 p 来说,k1 大于 k2 时,ceil(p / k1) 一定小于等于 ceil(p / k2)。把数组里所有堆的时间加起来,不等关系仍然保持。所以总耗时是关于 k 的单调不增函数。

在我们关心的区间内,一定存在一个最小的 k0,使耗时小于等于 H,而所有比 k0 小的 k 都做不到。这就是一个标准的最小型可行解问题。用二分思路就是:取中点 mid,如果 mid 这个速度能在 H 小时内吃完,说明答案可能还在左边,把右边界收紧到 mid;如果 mid 速度不行,说明答案一定在右边,把左边界收紧到 mid+1。最后 left 就是答案。

3.4 左右边界的取值到底怎么定

左边界取1,因为速度至少是每小时1根。右边界取 max(piles),而不是数组总和。为什么?因为当速度大于等于单堆最大数量后,每一堆都只需要1小时,总耗时就是数组长度,再增大速度不会让总耗时继续减少。既然更大速度对结果没有优化意义,最优解就不可能超过 max(piles)。这一步很多人写错,右边界直接取 sum(piles),虽然结果有时也能过,但会白白扩大二分范围,而且思路解释起来不够干净。

还有一个细节,如果 H 小于数组长度,说明就算每小时吃掉一堆也来不及完成,直接不可能。但二分代码其实能自动处理:即使右边界取 max(piles),判定函数也会返回 false,最终 left 会跑到 max(piles)+1。不过为了代码语义更清晰,可以在一开始加一句判断,避免无意义的二分。

4. 手写代码与运行实例

4.1 Python 版本与逐行解释

import math from typing import List class Solution: def minEatingSpeed(self, piles: List[int], h: int) -> int: # 特别情况:香蕉堆数比小时数还多,必不可能 if len(piles) > h: # 虽然二分也能处理,但提前判断可以让逻辑更清晰 # 这里不直接返回,因为后面会自然收敛,但写上无妨 pass def can_finish(k: int) -> bool: total_hours = 0 for p in piles: total_hours += (p + k - 1) // k return total_hours <= h left = 1 right = max(piles) while left < right: mid = (left + right) // 2 if can_finish(mid): right = mid else: left = mid + 1 return left

这里最关键的代码是 (p + k - 1) // k,它实现了向上取整。很多新手写成 int(math.ceil(p / k)),功能没错,但浮点数运算在大数场景下可能有精度损耗,而且调用库函数有额外开销。用整数运算表达向上取整,是面试中更稳妥的习惯,也是一个值得记住的模板公式。

while 循环用的是左闭右开变体,即 left 是可行答案的下界,right 是当前发现可行上界,循环结束时 left 等于 right。注意当 can_finish(mid) 为真时,mid 本身可能是答案,所以 right = mid 而不是 mid - 1;当 can_finish(mid) 为假时,答案至少是 mid + 1,所以 left = mid + 1。这样写可以完全避免死循环。

4.2 一个具体数组的二分推演

我们拿经典测试用例 [3, 6, 7, 11],H = 8 来手动走一遍,确保你理解整个搜索过程。数组最大值是 11,所以 left = 1,right = 11。

第一次二分,mid = 6。速度6时,第一堆耗时 ceil(3/6)=1,第二堆 ceil(6/6)=1,第三堆 ceil(7/6)=2,第四堆 ceil(11/6)=2,总耗时6小时,小于等于8,说明速度6可行,right 更新为6。

第二次二分,区间变成 [1,6],mid = 3。速度3时,四堆耗时分别为1、2、3、4,总耗时10小时,大于8,说明速度3不可行,left 更新为4。

第三次二分,区间变成 [4,6],mid = 5。速度5时,四堆耗时分别为1、2、2、3,总耗时8小时,等于8,可行,right 更新为5。

第四次二分,区间变成 [4,5],mid = 4。速度4时,四堆耗时分别为1、2、2、3,总耗时8小时,等于8,可行,right 更新为4。

此时 left 等于 right 等于4,循环结束,返回4。这个推演过程建议你在白板上也画一遍,面试时和面试官讲题,不用画得像图解那样精美,但一定要能口算出每一步,这会显得你是真的理解而不是背题。

4.3 复杂度分析

二分外层循环执行 O(log(max(piles))) 次,每次调用判定函数都遍历一遍数组,所以时间复杂度是 O(n log max(piles)),n 是数组长度。空间复杂度是 O(1),只用了几个临时变量。

对比暴力枚举 O(max(piles) * n),在 max(piles) 很大的情况下,二分优化是指数级的。很多数据规模大的题目,核心考点就是要你识别出这个二分答案结构,而不是真的考察你怎么又快又准地计算向上取整。

4.4 同模型的题目还能套到哪里

875 题是二分答案模型里最好讲的一道,因为它故事简单、判定函数清晰。同模型的题刷多了你会发现套路完全一样,换的只是“判定函数”的内容。比如 LeetCode 1011 在 D 天内送达包裹的能力,给你一个包裹重量数组,求最小载重能力,判定函数就是按顺序装包裹需要的天数是否超过D。还有 LeetCode 410 分割数组的最大值,要最大化最小值,本质上也是二分答案。

LeetCode 1482 制作 m 束花所需的最少天数,以及 LeetCode 2064 最大化一台游戏机的最小分数,都是类似模型。面试经典150里这类题并不少,你把875彻底吃透,再去做其他二分答案题,基本就是把判定函数换一换的事。

我自己刷题有个习惯:每做完一道题,就去题解区找同模型题再做一道,形成“一串题”而不是“一道题”。如果你能在三天内连续做4到5道二分答案题,你会发现识别题型的神经反射会明显变快,这比隔三差五做一道效果强得多。

5. 面试官真正想听到的表达方式

5.1 先证明单调性,再提出二分

很多候选人一上来就写二分代码,面试官问他为什么能用二分,他说因为数据量大。这个回答只能拿及格分。正确的表达顺序应该是:先明确指出这个问题是一个最优化问题,求最小速度;然后说明这个最优化问题存在单调性,速度越快总耗时越短;最后才提出用二分把最优化问题变成判定问题。

单调性的证明不需要写得像数学论文。你可以说,对任意一堆香蕉,速度从 k 变成 k+1,吃完这一堆所需时间一定不会增加,因此总耗时一定不会增加。这其实就是一个反证法的直觉化版本,面试官通常点个头就过了。

5.2 边界条件是你和面试官拉开差距的地方

边界条件往往是面试官最喜欢追问的点。常见的边界包括:左边界为什么是1而不是0;右边界为什么是 max(piles) 而不是 sum(piles);向上取整为什么用 (p + k - 1) // k;二分循环里等号应该加在哪边。

还有一个隐藏边界是:如果 H 恰好等于数组长度,答案就是 max(piles),因为这时候必须每堆都花一小时。如果 H 小于数组长度,任何速度都来不及,这时可以直接返回一个无意义值或者明确说明无解。虽然 LeetCode 原题保证有解,但面试官可能会改编成“如果无解怎么办”来考你,思考一下总没坏处。

5.3 一个现场追问:如何验证你的代码没有死循环

面试官可能会问,mid = (left + right) // 2 会不会死循环。这个问题在二分答案模板里很容易答:当 left + 1 等于 right 时,mid 等于 left,如果 can_finish(left) 为真,right 变成 left,循环结束;如果为假,left 变成 left+1,也就是 right,循环也结束。因为 two分支都会让区间缩小,所以不会死循环。

如果你用 left + (right - left) // 2 的方式写,还可以顺带提一句这是为了防止 left + right 溢出。Python 整数无上限不需要担心,但 C++ 或 Java 里可能会被追问,准备一个安全的写法总是加分项。

5.4 与热门100和周赛题的联动思考

热门100题里也有几道二分查找题,比如搜索旋转排序数组、在排序数组中查找元素的第一个和最后一个位置,它们考的是“在一个有序或半有序数组中查找目标值”。875 考的是“在答案域上二分”。两类题型如果用一句话区分,就是前者搜索空间是数组下标,后者搜索空间是数值范围。

周赛题偶尔也会出现二分答案,但周赛经常把题目包装得比较复杂,比如加一个很难的判定函数或者前置贪心。做周赛题更大的价值是练习读题速度和抗压能力,而不是学习新模板。拿周赛430的某些题来说,它的难度可能远超经典150的平均水平,我个人的策略是:经典150打底,周赛只做自己能力范围内能稳定AC的题,不在偏门上消耗精力。

6. 常见问题与踩坑实录

6.1 本地跑得好好的,一提交就超时

我见过很多人在875上超时,原因几乎都出在判定函数里多做了没必要的事。比如判定函数里每次都对 piles 排序,或者用了 while 循环一层层减去 k 来模拟一小时一小时地吃,这些写法在功能上没错,但复杂度会退化。

模拟吃的正确姿势是直接用除法向上取整,而不是循环减。一小时一小时地减,对于数量为 10 的 9 次方的香蕉堆来说,即便速度也很大,循环次数也很可观。判定函数需要被调用 log 次,如果判定函数内部再套一个 n 复杂度操作,整个时间复杂度就会从 O(n log max) 变成 O(n log max) 的常数倍或者更差。

6.2 向上取整的精度坑

浮点数向上取整有一个经典问题:在极限情况下,大整数转成 float 可能丢失精度,导致 ceil 结果差1。例如某些语言里浮点数精度只有 53 位二进制,当 p 超过 2 的 53 次方时,p / k 的浮点表示不再精确。

最稳妥的做法永远是用整数运算实现向上取整:ceil(p / k) = (p + k - 1) // k。这个公式在任何语言里都成立,也不依赖浮点精度。刷 LeetCode 倒很少碰到这么大的数,但养成这个习惯能帮你避开不少隐藏雷区。

6.3 判断条件等号应该放在哪边

二分查找里等号归属是最大陷阱之一。875 的判定条件是 total_hours <= h,也就是说总耗时等于限制时间也算可行,等号要算在“可行”这一边。如果你写成 total_hours < h,会漏掉正好在边界上的答案,最终结果偏大。

反过来,有些题目问你“最大可行值”,等号归属逻辑可能又不同。判断等号归属没有万能口诀,唯一方法是每次写代码时明确自己想找的是“最大可行”还是“最小可行”,然后推演一遍样例。我一般会在代码旁边注释一行“可行 = 耗时 <= 限制”,提醒自己别把条件写反。

6.4 死循环和左右边界混淆

当你已经用二分做了几道题,突然某道题卡在死循环里,很大概率是你把左闭右闭和左闭右开混用了。875 这个模板里,left 始终是候选答案,right 始终是已知或潜在可行上界,循环不变量必须保持一致。

如果 while left < right 但内部出现了 right = mid - 1,就可能漏掉 mid 本身。对于最小可行解问题,mid 可行时应该保守地把 right 收缩到 mid,而不是跳到 mid-1。同理,mid 不可行时 left 可以果断跳到 mid+1,因为 mid 已经被证明不可能是答案。这套逻辑推明白之后,二分答案基本不会再死循环。

6.5 刷到第73天如何对抗倦怠期

说句实在话,刷题刷到第70天左右是最容易放弃的阶段。基础题刷腻了,难题又啃不动,每天打开编辑器都不知道该干嘛。我的应对策略是降低“新题”数量,把当天大部分时间用在重写旧题上。比如875这道题,我在三周前就AC过,但今天重新写,要求自己不看任何提示,边写边讲,讲完再翻上次的打卡笔记对比差异。

这个过程很枯燥,但效果显著。重写旧题会暴露出很多“好像懂了其实没懂”的细节,比如右边界为什么是 max(piles),这个点在第一遍刷的时候我根本没真正理解,直到二刷要开口讲才发现解释不清楚。打卡的意义不是每天发一个“今日第X天”的仪式感,而是逼着自己在某个阶段把“我会做”变成“我能讲清楚”。

6.6 和周赛、热榜内容怎么平衡

如果你关注过最新热词,可能会看到“leetcode周赛430”这类内容频繁出现。我的态度是,周赛可以参加,但不要让它主导你的刷题计划。周赛题通常更追求新颖的组合和更复杂的优化,而面试经典150覆盖的是更本质的模型。今天做875,明天去看看周赛里有没有用二分答案的新题,有就顺手记一下,没有也没关系,这不影响主线进度。

还有一个常被忽略的问题:面试官很少会直接考周赛第四题的难度,却很可能拿875这种题进行变形追问。所以与其花三小时死磕一道周赛压轴题,不如把时间花在吃透经典150的二三题级别题目上,性价比会高很多。

7. 一些我踩过之后才明白的小技巧

最后分享几个只靠刷题很难总结出来的细节。第一,二分答案的代码模板一定要背到几乎形成肌肉记忆,这样到了面试高压环境下才能顺手写对,不用现场推理左右边界。第二,判定函数单独抽出来写,不要和二分主体混在一起,这样不仅代码结构清晰,面试时也方便单独讲判定函数的设计思路。第三,写完代码不要急着提交,先在脑内选一到两个极端用例跑一遍,比如全部香蕉在一堆里、H等于数组长度、piles全是同一个数,这能瞬间暴露边界问题。

我在实际使用中发现,很多人做这类题最大的障碍不是不懂二分,而是不会判断“这个题能不能用二分”。判断方法其实很简单:题目问的是最小或最大值,并且这个值的可行性与某个参数的单调性相关,那八九不离十就是二分答案。一旦识别出这个模型,剩下的工作就是设计判定函数。

这道875我刷了不止一遍,每次都会发现新的理解角度,第一次是背模板,第二次是搞懂右边界,第三次是能向别人讲清楚单调性证明。LeetCode面试经典150之所以值得慢慢刷,也是因为里面的题目经得起反复咀嚼。下次如果你也刷到第73天左右开始疲惫,不妨挑一道刷过的简单题,闭卷重写一遍,再对着镜子讲一遍,你会感受到一种比做新题更踏实的进步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 3:58:28

免费本地AI监控值守方案:YOLO目标检测与RTSP告警推送实战

各位做弱电、安防、运维的朋友&#xff0c;是不是都有过这种经历&#xff1a;监控大屏铺满一整面墙&#xff0c;几十上百个画面来回切&#xff0c;眼睛盯久了酸涩发胀&#xff1b;半夜施工现场怕进贼&#xff0c;小区车库怕剐蹭&#xff0c;仓库怕起火冒烟&#xff0c;值班室不…

作者头像 李华
网站建设 2026/9/8 3:58:23

代码有温度吗?从爱心动画到量化策略,换种视角看编程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 3:57:33

COMSOL锂电池热管理仿真全攻略:从建模到风冷/液冷/相变冷却对比

做电池热管理仿真这几年&#xff0c;我大部分时间都在跟COMSOL锂电池模型打交道。从最早的单体电芯三维热模型&#xff0c;到后来整包级别把风道、液冷板、相变材料堆叠在一起算热流耦合&#xff0c;这中间踩过的坑远比想象中多。很多刚入行的朋友问我&#xff0c;电池仿真到底…

作者头像 李华
网站建设 2026/9/8 3:57:10

AVA数据集下载全攻略:断点续传与并发加速的实战方案

简介&#xff1a;面向计算机视觉与美学质量分析研究者&#xff0c;该工具包提供基于Python的AVA数据集下载方案&#xff0c;支持通过Mega云盘或Torrent渠道批量获取约32GB、25万余张图片数据&#xff0c;省去手动逐包下载的麻烦。压缩包共30个文件&#xff0c;主要类型包括Pyth…

作者头像 李华
网站建设 2026/9/8 3:56:50

本地智能体部署全记录:Ollama+Dify从零搭建私有AI助手

本地部署大模型早就不是什么新鲜事了&#xff0c;Ollama 一行命令就能把 DeepSeek、Qwen 这类开源模型拉下来跑。但“跑模型”和“跑智能体”是两码事&#xff0c;后者需要一套能编排工具调用、记忆管理、多轮对话的框架。这篇“本地智能体部署全记录&#xff08;一&#xff09…

作者头像 李华