← 返回题目列表

Koko 吃香蕉为什么能二分速度?如何证明可行性单调?

高频 中等 第 13 / 26 题 更新于 2026/07/30
二分查找二分答案Koko吃香蕉单调性

简化版

Koko 吃香蕉二分的是“速度 k”,不是数组下标。速度越大,吃完所需时间越少;如果速度 k 能在 h 小时内吃完,那么所有更大的速度也能吃完,所以可行性满足单调性。对 k[1, max(piles)] 上二分,计算 sum(ceil(pile / k)),找第一个满足 hours <= h 的最小速度。

详细版

这题是典型二分答案:答案空间是吃香蕉速度 k,范围从 1 到最大堆香蕉数。判断函数是 can(k):以速度 k 吃完所有堆是否不超过 h 小时。每堆耗时是向上取整 (pile + k - 1) / k,因为一小时只能吃同一堆,剩一点也要占一小时。

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 (canFinish(piles, h, mid)) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}

boolean canFinish(int[] piles, int h, int k) {
    long hours = 0;
    for (int p : piles) hours += (p + k - 1) / k;
    return hours <= h;
}

复杂度是 O(n log M),M 是最大堆大小。面试重点是讲清楚单调性、上界为什么是 max(piles)、以及向上取整不能写成普通除法。

完整版教学

一、这题为什么不是在数组里找数

普通二分是在有序数组中找目标值,而 Koko 这题没有一个显式有序数组。它要求的是最小吃香蕉速度,速度 k 可以取 1 到 max(piles) 之间的整数。这个整数区间虽然不是输入数组,但它本身是有序的,且每个速度都能被判定为“可行”或“不可行”。

例如 piles=[3,6,7,11]h=8,速度 1 明显不行,速度 11 一定可以,因为每堆最多一小时。把每个速度标成是否可行,会出现一段 false 后接一段 true 的结构。二分答案就是在这个结构上找第一个 true。

k:       1  2  3  4  5  6  ...
can(k):  F  F  F  T  T  T  ...
目标:找最左边的 T

二、单调性来自“速度越快,耗时越少”

判断函数 can(k) 的核心是总耗时。速度 k 越大,每一堆需要的小时数 ceil(pile/k) 就不会增加,所以总耗时也不会增加。于是如果某个速度 k 可行,所有更大的速度都可行;如果某个速度不可行,所有更小的速度也不可行。

带数字走一遍:对 11 根香蕉,速度 3 需要 ceil(11/3)=4 小时,速度 4 需要 ceil(11/4)=3 小时,速度 6 需要 ceil(11/6)=2 小时。速度增加只会让耗时持平或下降,这就是二分能排除半边的原因。

速度 k3 根6 根7 根11 根总小时
3123410
412238
512238

记忆钩子:二分答案先问“答案越大,限制是更容易满足还是更难满足”,Koko 是速度越大越容易满足。

三、边界为什么是 1 到 max(piles)

下界是 1,因为速度为 0 没意义,且每小时至少吃 1 根才可能前进。上界可以取 max(piles),因为速度达到最大堆大小后,每一堆最多一小时吃完,再大也不会让单堆耗时低于 1 小时。也就是说超过 max(piles) 没有必要。

这不是拍脑袋取边界,而是根据答案的物理含义缩小搜索空间。若最大堆是 11,速度 12 和速度 11 的效果一样:11 那堆都是一小时,其他堆也都是一小时。因此 [1, max] 一定包含最优解,且不会漏。

lo = 1
hi = max(piles)
答案一定在 [lo, hi] 内

四、向上取整是最容易错的细节

每堆香蕉必须完整吃完,剩下一根也要额外占一小时,所以耗时是向上取整。整数代码里常用 (pile + k - 1) / k。例如 pile=7,k=3,普通除法得到 2,但实际需要 3 小时;公式 (7+3-1)/3 = 9/3 = 3 正好。

如果数据范围较大,累加总小时要用 long,因为 piles 数量多时总和可能超过 int。虽然很多平台 h 是 int,但中间累加溢出会导致可行性判断反向,二分就会收敛到错误答案。

long hours = 0;
for (int p : piles) {
    hours += (p + k - 1) / k;
}

五、为什么用 lower_bound 模板

我们要找的是第一个可行速度,也就是最左 true。因此适合使用半开或闭区间的 lower_bound 思路。这里用闭区间收缩到单点:while (lo < hi),若 mid 可行,说明答案可能是 mid 或更小,令 hi = mid;若 mid 不可行,答案只能更大,令 lo = mid + 1

这个写法的好处是循环结束时 lo == hi,它就是最小可行速度。不要在可行时直接返回 mid,因为 mid 只是一个可行解,不一定最小。二分答案题常见追问就是“为什么不是找到一个可行就停”。

can(mid) == true  -> [lo, mid] 里继续找更小可行
can(mid) == false -> [mid+1, hi] 里找可行

六、和其他二分答案题的共性

Koko、运送包裹、分割数组最大值这类题都是“最小化某个上限”。它们的共同套路是:猜一个答案 x,然后问“x 是否足够大,能满足约束”。如果 x 足够大,更大的 x 也足够;如果 x 不够,更小的 x 也不够,于是形成单调布尔数组。

区别只在 can(x) 怎么写。Koko 的 can(k) 是按速度计算总小时;运包的 can(capacity) 是按容量模拟天数;分割数组的 can(maxSum) 是按最大段和模拟段数。只要能证明单调性,二分就不是硬套模板。

题目二分对象可行性含义
Koko 吃香蕉速度 k总小时不超过 h
运送包裹船容量 cap天数不超过 D
分割数组最大值最大段和 limit分段数不超过 m

七、常见误区与追问

  • 误区:在 piles 数组上二分。 piles 是否有序不重要,真正有序的是速度答案空间。
  • 误区:每堆耗时用普通除法。 剩余香蕉也占一小时,必须向上取整。
  • 误区:找到可行速度就返回。 题目要求最小速度,要继续往左找第一个可行值。
  • 追问:为什么上界是 max(piles)? 速度超过最大堆不会让任何一堆低于 1 小时,收益为零。
  • 追问:复杂度怎么写? 每次判断扫 n 堆,二分速度范围 1 到 M,所以是 O(n log M)
  • 追问:如何识别这是二分答案? 看到“最小速度/容量/最大值”且可行性随答案单调变化,就优先考虑二分答案。

八、加强记忆

Koko 的主线是“速度答案空间 + 可行性单调”。先定范围 [1, max(piles)],再写 can(k) 计算向上取整总小时。can(mid) 为真就往左压,假就往右走,最终得到最小可行速度。记住它不是找数组元素,而是找第一个让约束成立的速度。