☰
Day4二分答案专题:从LeetCode 875吃透最小可行值套路
2026/10/4 4:33:04 网站建设 项目流程

1. Day 4 的由来:从"闷头刷题"到"按专题打深井"

LeetCode 打卡第四天,最大的感受不是"我会的题变多了",而是终于摸到了"套路"的边。前三天我是典型的乱序刷题选手:今天看到数组题就做数组,明天遇到链表就切链表,后天被一道字符串题卡得怀疑人生。结果呢?题号记了一堆,遇到新题照样无从下手。这种状态特别像我刚学做饭时的样子——菜谱背了二十道,但真让我不看菜谱做一桌菜,全乱套。因为菜谱是散的,没有形成"荤素搭配、先备菜再下锅"的流程感。

第四天我做了个重要调整:不再随机挑题,而是按专题进攻,一次只打一口井。今天这口井的名字叫"二分答案",主菜就是 LeetCode 热门 100 题里的第 73 题——爱吃香蕉的狒狒,对应原题编号 875。这道题绝对是二分答案专题的"教科书样本",因为它把"最小化一个值"的优化问题,干净利落地转换成了"给定一个值,判断行不行"的决策问题。这个思维转变一旦打通,后续整个专题的题都会顺畅很多。

1.1 为什么我把 Hot 100 当主线

LeetCode 热门 100 题这个清单,圈内评价一直两极分化。有人认为它太"经典"、不够前沿,但对我这种准备面试、需要快速建立题型地图的人来说,它几乎是必刷清单。原因很简单:这 100 道题覆盖了面试里出现频率最高的思维模型——双指针、滑动窗口、单调栈、二分、动态规划、图论基础,每一道都是某个套路的"标准件"。

我在 Day 1 到 Day 3 踩过最大的坑,就是"见题刷题、不归类"。数组题里有双指针,字符串里也有双指针,但我不归类就发现不了这个共性。Hot 100 的好处恰恰在于,它是按"经典模型"而不是"数据结构的表面标签"来分布的。你刷到第 73 题的时候会发现它本质是二分,而不是一道普通的"数组模拟题"。这个认知就是归类能力的雏形。

1.2 Day 4 的练习清单怎么排

今天的清单我做成了"一小套"递进结构,不是只刷一道题:

  • 前置热身:先默写标准二分查找的两套模板,确保"找左边界"和"找右边界"不会混。
  • 主菜:875 爱吃香蕉的狒狒,要求能在 20 分钟内独立写出来,并且用不同语言各写一遍。
  • 加餐:1011 在 D 天内送达包裹的能力,同样的套路,换了容器包装。
  • 佐餐:刚好赶上周赛 430,用里边的题目检验一下"二分答案"在实战里到底好不好使。

这个安排的逻辑是:先练肌肉记忆,再练识别迁移。光看懂题解没用,得靠大量重复让"看到最小化 xxx 就反射性想到二分"变成条件反射。

2. 原题复盘:LeetCode 875 爱吃香蕉的狒狒

2.1 题目到底在说什么

先讲人话。狒狒面前有 n 堆香蕉,每堆有 piles[i] 根,它每小时可以选择一堆,吃掉 k 根。如果这一堆剩下的不足 k 根,那它就把这一堆全部吃完,然后这个小时剩下的时间它就不吃了(对,它不会去再开一堆)。守卫走了,h 个小时后会回来。问:能让狒狒在 h 小时内吃完所有香蕉的最小整数速度 k是多少。

举个具体例子,piles = [3, 6, 7, 11],h = 8。

  • 如果 k = 4:每堆耗时分别是 ceil(3/4)=1、ceil(6/4)=2、ceil(7/4)=2、ceil(11/4)=3,总耗时 8 小时,刚好赶在守卫回来前吃完。
  • 如果 k = 3:耗时是 1 + 2 + 3 + 4 = 10 小时,超了。
  • 如果 k = 5:耗时是 1 + 2 + 2 + 3 = 8 小时,也能吃完,但它不是最小的,因为 4 已经可行了。

所以答案不是"找到一个能吃完的速度",而是"找到能吃完的最小的那个速度"。这句话是整道题的题眼。

2.2 从暴力法到二分答案:思维转变过程

很多新手拿到题第一反应是暴力枚举:从 k = 1 开始试,算总耗时,找到第一个满足条件的 k 就返回。这个思路一定对,但一定超时。因为 piles[i] 最大可以到 10^9,如果答案特别大,枚举的次数就会非常恐怖;再乘以每轮遍历 n 堆的开销,最坏情况是 O(max(piles) * n),在 LeetCode 的数据范围下直接 TLE。

那怎么优化?关键在观察"总耗时"和"速度"之间的关系。设想我们把速度 k 从 1 一路往上加,总耗时 f(k) 会怎么变化?肯定是不增的——吃得越快,花的时间越少,这不是什么高深数学,就是生活常识。这个"单调性"才是二分的灵魂。

有了单调性,题目就从"在无限空间里找最优解"变成了"在一个有序的布尔序列里找分界线"。我们定义一个 check(k):按照速度 k 能不能在 h 小时内吃完。那么 k 从小到大的 check 结果就是一堆 F、F、F、T、T、T……我们要找的答案,就是第一个 T 的位置。而"在一个单调序列里找第一个满足条件的位置",这正是二分查找最擅长的事。这个把"优化问题"翻译成"决策问题"的过程,就叫二分答案。

我特别喜欢这个题还有一个原因:它的 check 函数非常直观,不需要复杂的数学推导,只是老老实实地把每堆耗时加起来而已。

2.3 核心代码与复杂度分析

直接上 Python 实现,代码非常短:

class Solution: def minEatingSpeed(self, piles: List[int], h: int) -> int: left, right = 1, max(piles) while left < right: mid = (left + right) // 2 hours = sum((p + mid - 1) // mid for p in piles) if hours <= h: right = mid else: left = mid + 1 return left

这里 (p + mid - 1) // mid 是向上取整的经典写法。比如 p = 11,mid = 4,那 (11 + 3) // 4 = 3,正好是 ceil(11/4)。

复杂度分析:外层二分次数是 O(log(maxP)),maxP 是最大堆的香蕉数;每次 check 要遍历全部 n 堆,所以总复杂度 O(n * log(maxP)),空间 O(1)。这个复杂度在数据范围下非常轻松,跑得飞快。

3. 二分答案的完整模板与细节拆解

3.1 两套模板的区别与选择

很多人在二分这里翻车,翻车原因永远不是"不会二分",而是模板混用。我见过身边不少朋友把"找左侧边界"和"找右侧边界"的模板背串,结果在边界上调半天。这里我把两套最常用的模板整理成一张对照表:

模板适用场景核心写法注意点
左闭右闭 + 答案变量找到"某个值"或"最后一个满足条件的位置"while l <= r,满足条件时记录 ans 并收缩区间容易在收缩方向写反
左闭右开 + 区间收敛找"第一个满足条件的位置"while l < r,满足时 r = mid,不满足时 l = mid + 1mid 必须用下取整,不能 +1

875 这道题属于"找第一个满足条件的位置",所以用第二套模板最自然:left 指向"一定不行的区域外",right 指向"一定可行的区域"。每次把 mid 塞进 check,如果可行就把 right 收回来,如果不可行就把 left 推上去。循环结束时 left 和 right 重合,那个位置就是答案。

换句话说:布尔数组是 FFFTTT,我们要的是 F 和 T 之间的那条缝,这个缝就是 left 最终停下的地方。

3.2 check 函数怎么写才不容易错

这道题的 check 函数只有一行核心逻辑,但有几个容易写错的地方。

第一,向上取整不要用浮点。有人图省事写 math.ceil(p / mid),这在数值小时没问题,但 p 和 mid 都是大整数时,浮点精度会让结果产生 1 的误差,而且多一道类型转换,性能也不如整数运算。遇到这种"向上取整",我一律用 (p + mid - 1) // mid,纯整数运算,又快又稳。

第二,check 里的小优化:我们其实不需要算完所有堆的耗时,一旦累计 hours 已经大于 h,就可以提前 return False 了。这在数据量大时能省不少时间,尤其是二分后期 mid 很小时,几乎第一堆就超时了。写成这样:

def check(speed: int, piles: List[int], h: int) -> bool: total = 0 for p in piles: total += (p + speed - 1) // speed if total > h: return False return True

第三,别把 "hours <= h" 写成 "hours < h"。题目要求"在 h 小时内吃完",恰好用完 h 小时是允许的。这个等号丢掉的后果是:答案会凭空大 1,而且样例都不一定测得出来,非常阴险。

3.3 整数溢出与其他语言陷阱

Python 用户在这道题上很幸福,int 无限大,随便算。但如果你在用 Java 或者 C++,hours 这个变量就一定要用 long。为什么?piles 最多 10^4 堆,每堆最多 10^9 根香蕉,如果 k 很小,hours 理论上能累积到 10^13 这个量级,int 上限才 21 亿左右,直接爆。我最初用 Java 写的时候就是 int total,一提交就 WA,把 total 改成 long 立刻 AC。

另一个细节是二分的右边界。有人图省事把 right 设成 sum(piles),这在数学上没问题,但没必要——速度大于等于 max(piles) 时,每堆最多一小时就吃完了,再大速度没有意义。直接用 max(piles) 当上界,二分范围更小、收敛更快。

还有个小坑:left 一定从 1 开始,不要从 0 开始。左边界为 0 时,check 里 (p + 0 - 1) // 0 直接除零崩掉。这个错误低级但真实,我见过不止一个新手掉进去。

4. 把二分思想迁移到周赛 430 与同类题

4.1 周赛里的"最小可行值"套路

Day 4 刚好撞上周赛 430 的赛程,我打完之后最大的感触是:竞赛题和经典题之间的墙,比想象中薄得多。

周赛里常见一类题,描述五花八门:给你一个数组,让你做一些操作,问"最少操作几次能达成某个条件",或者"最小的某个阈值能保证 xxx"。很多人在赛场上看到这种题第一反应是贪心或者 DP,然后陷入细节调不出来。但如果你刚刷完 875,脑子里应该立刻弹出来一个问题:"这个量是不是单调的?如果我猜一个答案,能不能写一个 check 快速验证?"

比如一些题,答案的可能范围是 [1, max],check(mid) 的意思是"在限制为 mid 时能否完成目标",条件天然满足单调性。这时候就是二分答案的完美猎物。我在周赛复盘时发现,赛后题解里"二分答案 + check"的解法一抓一大把,而我自己在赛场上却绕了远路——这就是典型的不熟悉套路,导致识别不出来。

所以我的建议是:周赛的价值不只在 AC 数量,更在于赛后用经典题的目光去重新审视每一道题。你会发现 Hot 100 练的东西在真实比赛中是直接能用的,这种"经典题没白刷"的反馈,比任何打卡激励都管用。

4.2 同类题串讲:一个套路,多种包装

二分答案最有意思的地方在于,同一个套路能套进完全不同的故事背景里。我从 Hot 100 和相关题目里挑了三个典型的,放在一起对比看:

题目二分对象check 函数单调性来源
875 爱吃香蕉的狒狒吃香蕉速度 k总耗时是否 <= h速度越大耗时越少
1011 在 D 天内送达包裹的能力单日运载能力 cap所需天数是否 <= D运力越大天数越少
410 分割数组的最大值子数组和的最大值 limit能否在 <= m 段内分完limit 越大分完所需的段越少

以 1011 为例,核心思路一模一样:二分运载能力,猜一个 cap,然后从左往右贪心地装包裹,统计需要多少天,如果天数 <= D 就说明 cap 可行,否则不可行。唯一的区别就是把"吃香蕉耗时"换成了"运输天数",把"每小时一堆"换成了"每天必须按顺序装"。

我在刷 1011 的时候还发现一个细节差异:875 的左边界固定是 1,但 1011 的左边界必须是 max(weights),因为任何一天的运载能力如果小于单件包裹的重量,这件包裹就永远送不出去。这类"隐藏约束"是二分答案题的第二道陷阱,单靠模板是发现不了的。

4.3 怎么一眼识别"该用二分答案"

这个能力比多刷十道题都值钱。我的经验是,看到题目同时满足下面三条,就可以优先考虑二分答案:

第一条,问题是"最小化 xxx"或"最大化 xxx",且答案是一个有限范围内的整数。比如最小吃香蕉速度、最小运载能力、最小分割上限。

第二条,存在一个天然的 check 函数,也就是"给定一个候选答案,能在多项式时间内验证它是否可行"。这个验证过程往往伴随一次贪心扫描或者简单累加。

第三条,候选答案和验证结果之间有单调性。这一步最关键,也是很多人忽略的。你需要先证明:答案增大(或减小)时,check 的结果只会从 False 变 True 或者反过来,不会忽 True 忽 False。

一句话总结:题目问"最值",答案有界,check 好写,具备单调——四个信号凑齐三个以上,直接往二分的路子想。

5. 常见 bug 排查与调试技巧实录

5.1 问题速查表

刷这类题最容易踩的坑其实高度重复,排成一张速查表,贴屏保都行:

症状根因解法
提交后超时check 没有提前剪枝,全量求和累计超过上限立即 return false
答案比正确值大 1边界条件用了 < 而不是 <=检查"恰好耗尽 h 小时是否允许"
答案比正确值小二分右边界取小了确认上界是 max(piles) 或 max(weights)
死循环不退出mid 计算方式与区间收缩方向不匹配统一用 l + (r - l) // 2,检查收敛方向
Java/C++ 答案异常大hours 用 int 存储,溢出了中间量改用 long
运行时除零左边界从 0 开始左边界从 1 或业务下界开始

5.2 一次真实的翻车记录

这里分享一个我自己的真实 debug 过程。用 Python 写 875,第一版我写成这样:

l, r = 0, max(piles) while l < r: mid = (l + r) // 2 if sum((p + mid - 1) // mid for p in piles) <= h: r = mid else: l = mid + 1 return l

一运行,直接 ZeroDivisionError。当时我还有点懵,看了看报错行才反应过来:left 初始是 0,第一次 mid = (0 + 11) // 2 = 5,check 能过,然后 r 变成 5;接着 mid = (0 + 5) // 2 = 2,也正常;问题在于如果一开始返回 False,比如某些用例下 mid 可能会落回 0,然后第二行 p // 0 当场爆炸。所以 left 必须从 1 起步,最好顺手把 right 也压到 max(piles),减少无谓的迭代。

第二版我又踩了个逻辑坑。我把 check 条件从"总耗时 <= h"写成了"总耗时 < h",样例全过,但提交 WA 在某个隐藏用例上。原因是这个用例刚好要求狒狒在 h 小时内"恰好"吃完,而我把这个合法情况排除了,导致答案整体上偏大。找了好久才通过肉眼对比发现等号丢了——这种边界错误最坑人,因为它不在每个样例上都爆发。

排查这类问题,我的经验是:WA 之后不要急着翻题解,先把测试用例往极端方向构造。比如 h 等于堆数、piles 全是 1、piles 里有一个特别大的数。这三类用例基本能覆盖二分答案题 80% 的边界错误。

5.3 用暴力解当裁判,给二分答案做校验

这里分享一个我强烈推荐的调试习惯:写一个暴力参照函数,和二分答案版本在随机数据上对拍。

思路很简单。暴力版就是枚举 k 从 1 到 max(piles),逐个验证,虽然慢但正确性一目了然。然后用随机生成的 piles 和 h 去跑两版结果,一旦发现不一致,立刻定位问题。这个办法在刷题阶段特别好用,尤其是你对某个边界条件拿不准的时候。

import random def brute(piles, h): for k in range(1, max(piles) + 1): hours = sum((p + k - 1) // k for p in piles) if hours <= h: return k return -1 def binary_search(piles, h): l, r = 1, max(piles) while l < r: mid = (l + r) // 2 if sum((p + mid - 1) // mid for p in piles) <= h: r = mid else: l = mid + 1 return l for _ in range(10000): n = random.randint(1, 20) piles = [random.randint(1, 100) for _ in range(n)] h = random.randint(n, 100) assert brute(piles, h) == binary_search(piles, h) print("all ok")

对拍跑 10000 组随机用例,如果全过,基本可以放心提交。这比你自己脑补边界条件靠谱一百倍。后来我做 1011、410 的时候也直接用这套对拍框架,改一下 check 函数就能复用,省了很多事。

6. Day 4 收尾:二分答案之外,我学到的三件事

说实话,第四天给我最大的收获不是会了 875 这道题本身,而是三件比题更值钱的事。

第一,"套路"不是贬义词,它是经验的压缩包。前三天我总觉得 AI 味重的教程里讲"套路"很虚,但自己刷到第四天就明白了:二分答案、双指针、单调栈,这些名字背后都对应着一类被反复验证过的思维路径。你不需要每次从零发明解法,你需要的是快速识别题目的"骨架",然后往骨架里填肉。

第二,刷题的量要建立在复盘的质量上。一道题刷完,如果只是"AC 了就划走",等于白刷。我会强制自己回答两个问题:这道题卡在哪一步?我这个思路能不能迁移到上一周做过的那道题上?答不上来就回去重刷。Day 4 的 875 和 1011 放在一起对比之后,我对"二分答案"这个模型的记忆深度,比单独刷十道题都深。

第三,也是今天最后想分享的一个小技巧:写题解。不是写给别人看的那种正式题解,而是用几句话把这个题的思路讲给自己听。我在 Day 4 复盘时写的一句话是:"找最小值,就猜一个值然后验证,验证结果跟着猜的值单调变化——这就是二分答案的生活原型:你猜一个速度,跑得动就再猜慢点,跑不动就猜快点,直到找到刚好跑不动的那个临界点。" 把这个"人话版本"写下来之后,我发现自己对二分答案的理解突然就立体了。

明天是 Day 5,我计划进入"双指针与滑动窗口"专题,正好把前几天的二分单调性和窗口收缩再串一串。刷题这事,贵在细水长流。第四天,阶段性及格。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询