LeetCode LCR 073. 爱吃香蕉的狒狒
题目描述
题意分析
有若干堆香蕉,警卫会在
h小时后回来。每小时可以挑一堆吃,吃掉至多k根;如果这堆不足k根,就把这堆吃完,这一小时也不会再去吃别的堆。要求返回能在h小时内吃完全部香蕉的最小速度k。「吃不满也要占满一小时」这条规则是全部计算的基础。它意味着一堆
p根香蕉在速度k下需要 $\lceil p / k \rceil$ 小时,各堆之间互不共享时间,总耗时就是各堆向上取整之和。要找的是最小的可行速度。速度越大耗时越短,「速度
k能否在h小时内吃完」这个性质随k增大只会从假变真一次——这正是二分答案的信号。题目求的是第一个为真的位置。约束里堆数可达 $10^4$、单堆香蕉数可达 $10^9$,
h不小于堆数。堆数与单堆数量的量级差异说明:不能对速度做线性试探(值域到 $10^9$),但可以在每次判定里花 $O(n)$ 扫一遍数组,总代价 $O(n \log \max)$ 完全可接受。边界上要注意:
h至少等于堆数,所以答案一定存在(速度取最大堆时,每堆恰好一小时,总耗时等于堆数,必定不超过h);答案至少是 1,至多是最大堆的香蕉数,再大也没有意义。
解法:二分查找判定答案
核心思路
暴力做法是从
k = 1开始逐个试,每个k算一次总耗时,第一个满足<= h的就是答案。代价是 $O(n \cdot \max)$,在最大堆达 $10^9$ 时完全不可行。瓶颈在于线性试探每次只排除一个速度,而「可行性」显然是单调的:若速度
k能按时吃完,那么任何比k大的速度都能(每堆的耗时只会更少或持平);若k不行,任何更小的速度更不行。这个单调性让一次判定就能砍掉一半值域。于是把速度的取值域
[1, max(piles)]当作搜索区间。下界取 1 是因为速度必须为正;上界取最大堆,是因为速度再大也不会让任何一堆少于一小时,总耗时已经降到堆数这个下限,继续增大毫无收益。判定函数是:给定速度
mid,累加每堆的 $\lceil p / mid \rceil$ 得到总耗时,与h比较。向上取整用整数写法(p + mid - 1) / mid,避免浮点误差。维持的不变量是:答案始终落在
[left, right]内;left左侧的所有速度都不可行,right及其右侧的所有速度都可行。判定为真(可行)时保留mid(right = mid),因为它自己可能就是最小可行速度;为假时排除(left = mid + 1),因为它和更小的速度都不行。收敛后的left就是答案。
解题步骤
- 先扫一遍求出最大堆的香蕉数作为搜索上界。用最大堆而不是总和,是因为速度超过最大堆之后总耗时不再变化,多出来的值域纯属浪费;上界越紧,二分轮数越少。
- 令
left = 1、right = max。下界必须是 1 而不是 0,速度为 0 会在判定里除以零。- 循环条件写
left < right,收缩到唯一候选时退出。- 每轮取
mid = (left + right) >>> 1,然后花 $O(n)$ 遍历所有堆,累加(pile + mid - 1) / mid得到总耗时s。整数向上取整必须这样写,用Math.ceil(pile * 1.0 / mid)在大数上会有浮点精度风险。- 若
s <= h,速度mid可行,答案不会更大,令right = mid。保留mid是因为它有资格当答案,排除就会漏掉最优解。- 否则
s > h,mid太慢,它和所有更小的速度都出局,令left = mid + 1。- 退出时
left == right,返回left,即最小可行速度。不需要再验证一次,不变量已经保证了它可行且它减一不可行。以
piles = [3, 6, 7, 11]、h = 8走一遍:最大堆是 11,搜索区间[1, 11]。第一轮mid = 6,耗时为 $\lceil 3/6 \rceil + \lceil 6/6 \rceil + \lceil 7/6 \rceil + \lceil 11/6 \rceil = 1 + 1 + 2 + 2 = 6 \le 8$,可行,right = 6。第二轮mid = 3,耗时 $1 + 2 + 3 + 4 = 10 > 8$,太慢,left = 4。第三轮mid = 5,耗时 $1 + 2 + 2 + 3 = 8 \le 8$,恰好卡满,可行,right = 5。第四轮mid = 4,耗时 $1 + 2 + 2 + 3 = 8 \le 8$,仍然可行,right = 4。此时left == right == 4,返回 4。验算速度 3 时耗时 10 超限、速度 4 时耗时 8 达标,4 确实是最小可行速度。
代码实现
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int mx = 0;
for (int pile : piles) {
mx = Math.max(mx, pile);
}
int left = 1, right = mx;
while (left < right) {
int mid = (left + right) >>> 1;
int s = 0;
for (int pile : piles) {
s += (pile + mid - 1) / mid;
}
if (s <= h) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
}
func minEatingSpeed(piles []int, h int) int {
left, right := 1, slices.Max(piles)
for left < right {
mid := (left + right) >> 1
s := 0
for _, pile := range piles {
s += (pile + mid - 1) / mid
}
if s <= h {
right = mid
} else {
left = mid + 1
}
}
return left
}
复杂度分析
- 时间复杂度:$O(n \log M)$,其中 $n$ 是堆数、$M$ 是最大堆的香蕉数。二分把值域从 $M$ 收敛到 1 需要 $O(\log M)$ 轮,每轮的判定要遍历全部堆做一次 $O(n)$ 的累加。
- 空间复杂度:$O(1)$,除了几个整数变量没有任何额外结构,判定过程原地累加不需要辅助数组。
关键点总结
- 「求满足条件的最小/最大值」且「条件随参数单调」时,就该想到二分答案。把题目从「直接构造最优解」改写成「反复判定某个候选是否可行」,往往能把难题降成模板题。
- 二分答案的三个要件要逐一确认:值域的上下界、判定函数、单调性的方向。本题的判定是 $O(n)$ 的贪心累加,单调方向是「速度越大越可行」。
- 上界要取「使问题平凡可行的最小值」,这里是最大堆而不是总香蕉数。上界越紧,轮数越少,也更容易说服面试官你想清楚了边界。
- 整数向上取整写成
(a + b - 1) / b,永远不要用浮点除法再取整。大数上的浮点误差会造成极难复现的偶发错误。- 判定为真时保留
mid、为假时排除,是「找第一个为真」的固定规则;配合left < right与返回left,整套模板不需要任何循环后的补充判断。- 面试视角:先说清「速度与可行性的单调关系」再写代码,这是判卷的核心;很多人直接写二分框架却说不出为什么单调,会被追问到卡住。
- 面试视角:常见追问是「如果允许一小时内吃多堆呢」。此时总耗时变成 $\lceil \sum p / k \rceil$,单调性依旧但判定式简化,可以直接解析求解不必二分——借这个对比说明「吃不满也占满一小时」这条规则才是本题的难点来源。
易错点总结
- 错误写法:搜索下界取 0。用例
piles = [3]、h = 1→ 判定时执行(3 + 0 - 1) / 0,除零异常。速度必须为正整数。- 错误写法:搜索上界取所有香蕉之和。用例
piles = [1000000000] * 10000→ 值域从 $10^9$ 膨胀到 $10^{13}$,二分轮数增加且int存不下,需换long,纯属自找麻烦;速度超过最大堆后耗时不再下降。- 错误写法:向上取整写成
pile / mid忘了进位。用例piles = [3, 6, 7, 11]、h = 8→ 耗时被算成 $0+1+1+1=3$,误判速度 6 甚至更小的速度可行,返回值偏小。- 错误写法:向上取整用
(int) Math.ceil(pile * 1.0 / mid)。用例piles = [1000000000]、mid = 1→ 大数转double存在精度损失,个别数据上取整结果偏差 1 小时,导致边界速度被误判。- 错误写法:可行时写
right = mid - 1。用例piles = [3, 6, 7, 11]、h = 8→ 正确答案 4 在某轮成为mid后被排除,最终返回 5。- 错误写法:不可行时写
left = mid。用例piles = [3]、h = 1→ 两元素区间里mid恒等于left,区间不收缩,死循环。- 错误写法:判定条件写成
s < h。用例piles = [3, 6, 7, 11]、h = 8→ 耗时恰好等于 8 的速度 4 被判为不可行,返回 5,正确答案是 4;题目是「在h小时内」,含等号。- 错误写法:把总耗时累加进
int而不考虑规模。用例piles有 $10^4$ 堆、每堆 $10^9$ 根、mid = 1→ 总耗时达 $10^{13}$,int溢出成负数被误判为可行,返回 1。可以在累加中途一旦超过h就提前返回,既防溢出又剪枝。- 错误写法:认为答案就是「总香蕉数除以
h向上取整」。用例piles = [3, 6, 7, 11]、h = 8→ 总数 27 除以 8 上取整得 4,凑巧正确;但piles = [30, 11, 23, 4, 20]、h = 6时该式给出 15,正确答案是 23,因为不能跨堆吃。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 875. 爱吃香蕉的珂珂 | 中等 | 与本题同题,是二分答案配合 $O(n)$ 判定的标准样板 |
| 1011. 在 D 天内送达包裹的能力 | 中等 | 判定改为按顺序贪心分段,下界必须取单件最大值否则无解 |
| 410. 分割数组的最大值 | 困难 | 最小化最大段和,判定同为贪心计数,也可用区间 DP 对照 |
| 1482. 制作 m 束花所需的最少天数 | 中等 | 二分天数,判定需统计连续可用段,且要先判断整体是否有解 |
| 1552. 两球之间的磁力 | 中等 | 最大化最小间距,单调方向与本题相反,收缩规则要整体镜像 |
| 1231. 分享巧克力 | 困难 | 同为最大化最小值,判定是贪心累加分块并统计块数 |
| 69. x 的平方根 | 简单 | 判定退化成一次乘除,可用来对照左右边界两套取整与收缩的配对 |