news 2026/10/2 15:16:58

LeetCode 3296:移山最少秒数——二分答案与产能判定全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 3296:移山最少秒数——二分答案与产能判定全解析

1. 先把这个“移山”问题翻译成人话

1.1 题目到底在算什么

前两天刷题碰到一道题,名字挺唬人,叫“移山所需的最少秒数”,就是 LeetCode 3296。题目描述其实很简短:给一座山,高度是mountainHeight,再给一组工人workerTimes,第i个工人移除第j单位高度需要workerTimes[i] * j秒。也就是说,每个人搬第一块土用时固定,搬第二块要两倍时间,搬第三块要三倍时间,越往后越慢。问的是所有工人一起开工,最少要多少秒才能把山移平。

我第一次看到这个题,第一反应是“这有什么难的,总高度除以总速度不就完了吗”。等我真正动手推公式才发现,事情完全不是那样。因为每个工人的产出率不是一个常量,而是随着自己已经搬走的块数逐步下降。一个人搬 k 块土的总耗时不是w * k,而是:

T(k) = w * (1 + 2 + ... + k) = w * k * (k + 1) / 2

这是一个二次增长,不是线性的。所以这道题表面是个“调度安排”题,内核其实是“二分答案 + 数学判定”的典型题。

这篇文章不是单纯念一遍题解,我会把自己从读题到写出正确代码的完整思考过程、卡过的边界、以及为什么这个模型在工程里也经常出现,全部讲清楚。如果你在准备算法面试,或者正在刷二分答案类题目,这篇文章应该能帮你少走很多弯路。

1.2 为什么不能直觉地“按速度”除一下

很多人看这个题,第一反应是:每个工人每秒能搬1 / workerTimes[i]的高度,把所有工人的速度加起来,然后用mountainHeight / 总速度不就是答案吗?

这个想法错得挺隐蔽。原因在于工人搬第 j 块土的耗时是变化的,不存在一个固定的“每秒搬多少块”的恒定速度。你只能说他“搬完前 k 块土总共用了多少秒”。如果把时间切成无限小段,每一秒的“瞬时产出”都在变化,根本没法用一个常数速率去算。

更关键的是,多个工人并行时,不同工人的“下一块土耗时”也不一样。比如工人 A 搬下一块要 5 秒,工人 B 搬下一块要 1 秒,那么在同一个时间窗口内,哪个工人应该继续干活,哪个工人应该休息,这种动态分配问题如果直接模拟,会非常麻烦。本质上,我们要求的是把mountainHeight个单位分配给各个工人,使得每个工人自己的累计耗时尽量均衡,并且总体最大耗时最小。

所以这道题不能靠贪心模拟,也不能靠简单除法,得换一种思路。

1.3 换个思路:问自己“T秒够不够”

我后来意识到,这道题真正的突破口是:把“求最少秒数”变成一个“给定秒数,判断是否可行”的问题。

假设现在给你一个候选答案T,问你:如果只给所有工人T秒,他们能不能把整个山移完?这个问题好回答多了。因为每个工人在T秒内的产出上限是确定的,我只需要把每个工人T秒内最多能搬多少块土算出来,加总,然后看总数有没有达到mountainHeight。

而这个“猜一个时间,再验证够不够”的过程,正好就是二分答案。时间越短越难完成,时间越长越容易完成,答案一定落在“不可行”和“可行”的分界点上。于是问题从“怎么安排工人”变成了“怎么快速验证”。

这个思维切换,才是整道题最值钱的地方。

2. 核心解法:二分时间 + 产能判定函数

2.1 单调性是我们敢二分的理由

二分答案能成立的前提是单调性。在这道题里,单调性非常直观:如果T秒能完成任务,那么任何比T更大的时间也一定能完成任务。反过来,如果T秒完不成,更短的时间也完不成。换句话说,可行性随着时间增加而从“否”变成“是”,只会翻转一次。

这个性质有点像猜数字游戏:区间左边是“太小”,区间右边是“够大”,每次取中间值问一次,如果是加大就向左半边找,如果还不够就向右半边找。二分答案的时间复杂度是O(log R),其中R是答案的取值区间长度。哪怕区间长度是10^18,二分六十轮也就结束了,这才是这个方案能扛住大数据的关键。

我用一个生活化的例子帮你理解。假设你要烧一壶水,不知道要几分钟,但你有一个温度计。你先猜 5 分钟,水没开;再猜 10 分钟,水开了。那答案一定在 5 到 10 之间。接下来猜 7 分钟,还没开;猜 8 分钟,刚好开;猜 9 分钟,也开。于是你知道最少需要 8 分钟。这和我们验证“给定 T 秒够不够”是完全一样的逻辑。

2.2 给定T秒,算出每个工人能干多少活

现在关键来了:怎么快速算出一个工人在T秒内最多能搬多少块土?

设这个工人w最多能搬x块土,那么“搬完 x 块土的总耗时”必须不超过T。公式是:

w * x * (x + 1) / 2 <= T

很多人会直接在这里用乘法硬算,但其实有一个非常关键的防溢出技巧:两边同时除以w。因为w是正整数,左边是w * (x * (x + 1) / 2),右边是T,所以原不等式等价于:

x * (x + 1) / 2 <= floor(T / w)

令limit = T / w,剩下的问题就是解一个一元二次不等式:

x^2 + x - 2 * limit <= 0

求根公式给出:

x = floor( (sqrt(1 + 8 * limit) - 1) / 2 )

为什么这里强调T / w而不是T?因为w * x * (x + 1) / 2在w和x都很大的时候,可能超过 64 位整数的范围。而先用T / w,所有中间值都被限制在T这个量级附近,安全得多。

理论上公式算出来的x就是最大可搬块数。但实际写代码时,由于浮点数开方存在精度误差,直接拿公式结果去用,可能在边界上差 1 到 2 个。所以稳妥的做法是:先用公式算一个近似值,再用一两个while循环把边界修到正确。

2.3 check函数的三个防爆细节

完整的check函数要做三件事:第一,对每个工人算出x_i;第二,把所有x_i累加;第三,判断累加结果是否达到mountainHeight。这个过程里有三个细节,我建议你做面试题时也养成习惯。

第一个细节,就是前面提到的“左右两边先除以w”。这个变形不只是防止溢出,还能让公式里的limit位数变小,计算更快。

第二个细节,累加total时,一旦total >= mountainHeight,立刻return true。一方面省时间,另一方面防止total无限累加导致溢出。因为题目只关心“够不够”,不关心“超出多少”。

第三个细节,计算x的时候,初始值可以给一个“偏大”的估计,然后只向下修正。这样while循环的次数是常数级,不会引入额外复杂度。我的习惯是取:

long long x = sqrt(2.0 * limit) + 2;

然后向下修正:

while (x * (x + 1) / 2 > limit) x--;

如果浮点误差导致初始值比真实值小,那再加一个向上修正:

while ((x + 1) * (x + 2) / 2 <= limit) x++;

注意“向下修正”和“向上修正”的顺序,以及循环条件,都要写对。我见过不少同学把这个写成死循环,后面我会专门讲这个坑。

3. 完整代码实现与复杂度分析

3.1 C++ 版本(带注释)

完整的 C++ 解法如下,我加了注释,方便你直接对着看:

class Solution { public: long long minimumSeconds(int mountainHeight, vector<int>& workerTimes) { auto can = [&](long long t) -> bool { long long total = 0; for (int w : workerTimes) { // 关键:两边同时除以 w long long limit = t / w; // 解不等式 x*(x+1)/2 <= limit // x 初始给一个偏大的估计,然后向下修正 long long x = (long long)(sqrtl((long double)2 * limit)) + 2; // 如果算小了,向上修 while ((x + 1) * (x + 2) / 2 <= limit) { x++; } // 如果算大了,向下修 while (x * (x + 1) / 2 > limit) { x--; } total += x; // 一旦够了就直接返回,既加速又防止 total 溢出 if (total >= mountainHeight) { return true; } } return false; }; long long left = 1, right = 1e18; while (left < right) { long long mid = left + (right - left) / 2; if (can(mid)) { right = mid; } else { left = mid + 1; } } return left; } };

这里解释几个代码细节。

sqrtl((long double)2 * limit)用的是长双精度开方,精度比普通double高。为什么不用2.0 * t / w?因为limit本身就是t / w,直接用2 * limit更简洁,也避免再一次做大数除法。初始值故意+2,就是为了保证它大概率大于等于真实解,这样向下修正的循环次数很少。

right = 1e18是这道题原题约束下的一个安全上界。如果面对的是竞赛环境,我建议用倍增法初始化右边界,后面我会写。

二分循环里用left + (right - left) / 2而不是(left + right) / 2,是为了防止两个大数相加溢出。虽然这道题里1e18相加不会爆long long,但是这是一个通用好习惯。

3.2 Python 版本(用 isqrt 防浮点误差)

Python 版本我推荐用math.isqrt,它返回的是整数平方根,完全没有浮点误差,非常适合这种边界精确的公式:

from math import isqrt from typing import List class Solution: def minimumSeconds(self, mountainHeight: int, workerTimes: List[int]) -> int: def can(t: int) -> bool: total = 0 for w in workerTimes: # 同样先除以 w limit = t // w # 精确整数平方根,避免浮点误差 x = (isqrt(1 + 8 * limit) - 1) // 2 # 边界修正 while x * (x + 1) // 2 <= limit: x += 1 while x * (x + 1) // 2 > limit: x -= 1 total += x if total >= mountainHeight: return True return False left, right = 1, 10**18 while left < right: mid = (left + right) // 2 if can(mid): right = mid else: left = mid + 1 return left

用isqrt是这个小节的一个小技巧。很多人会用int(sqrt(...)),但浮点sqrt在某些边界数字上会往回缩一位。比如sqrt(225)在 IEEE 754 标准里一定是 15,但sqrt(226)的浮点结果可能比真实值略小,取整后差一。isqrt直接给出精确值,配合边界修正,代码更稳。

Python 整数没有固定位数,理论不上不会溢出,但t // w这种做法依然值得保留,原因不只是防溢出,而是它让公式里的数字更小,计算更快。

3.3 复杂度与边界说明

这套解法的时间复杂度是O(n log R),其中n是工人数量,R是答案区间长度。这里R大约是10^18,log2(R)约等于 60,所以外层的二分只有六十轮。每一轮里要遍历所有工人,计算每个工人的最大产出。空间复杂度是O(1),没有额外数组。

对比一下堆模拟方案:如果你用最小堆,每次让一个工人多搬一块土,复杂度是O(mountainHeight * log n)。如果mountainHeight是10^5,堆模拟还勉强能跑,但如果mountainHeight到10^9,那就完全不可接受了。二分答案厉害的地方在于,它把对“高度 H”的依赖变成了对“答案范围 R”的依赖,而log R增长非常缓慢,这才是大数据下真正的解法。

关于二分上界,我再补充一个通用写法。如果你不想依赖题目约束,可以用倍增法初始化右边界:

long long left = 1, right = 1; while (!can(right)) { right *= 2; }

这样right会从 1 开始翻倍,直到找到一个足够大的可行时间。好处是完全不用猜上界,坏处是如果答案非常大,right可能会爆long long。不过原题约束下没有这个问题。

4. 实战中我踩过的坑和排查技巧

4.1 一上来就写乘法,直接溢出

我写第一版代码的时候,check函数里直接写了:

if (1LL * w * x * (x + 1) / 2 <= t) ...

结果在小数据样例上能过,一到大测试用例就各种奇怪错误。原因很简单:当w和x都达到10^9量级时,w * x * (x + 1)早超过long long的表示范围了,哪怕最后还要除以 2,中间的乘积已经爆炸。

正确的做法应该是先把不等式两边同时除以w。我再次强调一下这个等价关系:因为w * (x * (x + 1) / 2)是w的整数倍,所以w * X <= t等价于X <= floor(t / w),这里X = x * (x + 1) / 2。因此你只需要算:

long long limit = t / w;

然后把所有判断都基于limit。这样一来,中间最大数字也只有x * (x + 1),而x的量级是sqrt(limit),即使limit是10^18,x * (x + 1)也只有10^18量级,刚好在long long安全范围内。

如果你真的担心极端情况,C++ 里可以用__int128做乘法判断,但原题完全不需要。

4.2 右边界取值太保守,答案被切掉

另一个让我折腾了一会儿的坑,是二分右边界。

我一开始觉得:最慢的工人搬一块土最多耗时max(workerTimes),山高是mountainHeight,那答案最多也就是mountainHeight * max(workerTimes)。于是把右边界设成了这个值,结果好几个测试用例直接报错。

后来我意识到,这个上界只对“每个人只搬一块土”的情况成立。如果只有一个人干活,他要搬完一整座山,总耗时是:

w * mountainHeight * (mountainHeight + 1) / 2

这个数字比mountainHeight * w大得多。举个例子,mountainHeight = 100,w = 100,那么一个人搬完 100 块土需要:

100 * 100 * 101 / 2 = 505000

而mountainHeight * w只有10000,差距巨大。所以用mountainHeight * max(workerTimes)作为右边界,会把正确答案直接切在二分区间外面。

后来我改成固定1e18,或者用倍增法找右边界,问题就消失了。这也是我推荐倍增法的原因,它不需要你事前估算答案的量级。

4.3 while边界修正写成了死循环

用公式算 x 之后,边界修正的代码也需要小心。我的一个朋友第一次写这个题,他在check里写了这样一段:

while (x * (x + 1) / 2 <= limit) { x++; } while (x * (x + 1) / 2 > limit) { x--; }

单看好像没问题,但实际运行起来在某些输入下会死循环。原因在于:如果浮点初始值恰好非常接近真实解,第一个循环把x加到某个值,使得x * (x + 1) / 2刚好大于limit,然后退出;第二个循环又把x减回去,使得x * (x + 1) / 2刚好小于等于limit,然后退出。看上去没问题,但因为边界上浮点修正常常出现“x 在真实值附近来回跳动”,如果两个循环的结束条件之间隔着不止一个整数,就可能反复横跳。

我的解决办法是:初始值故意加一个固定余量,保证x一定不小于真实值,然后只用“向下修正”。这样循环从大到小逼近真实值,最多执行常数次,绝不会来回跳。

具体写法是:

long long x = (long long)(sqrtl((long double)2 * limit)) + 2; while (x * (x + 1) / 2 > limit) { x--; }

如果你担心初始值偏小,就在前面加一个“向上修正”,但要确保两个循环的修正总量很小。实际浮点误差最多差 2 到 3,所以我的写法足够安全。

4.4 为什么堆模拟不是好答案

我见过有人用最小堆做这道题:把每个工人的“下一块土耗时”放进堆里,每次弹出耗时最小的工人,让他多搬一块土,然后把他的下一块土耗时压回堆里,直到总块数达到mountainHeight。这个过程在数据规模很小的时候完全正确,而且思路很直观。

但堆模拟的时间复杂度是O(mountainHeight * log n)。如果mountainHeight是10^5,大约要执行十万次堆操作,勉强能过。可一旦mountainHeight到10^6、10^7,堆模拟就彻底不行了。二分答案则完全不受mountainHeight大小影响,它的复杂度只由答案范围的对数决定。面试的时候,如果一道题的数据范围明文给出了mountainHeight可以很大,那你基本可以判断命题人希望考察的不是堆模拟,而是二分答案加数学判定。

5. 这套思路的价值不止于“移山”

5.1 抽象成资源分配模型

把“移山”这个外壳剥掉,本题其实是一个很常见的资源分配模型:有n个处理节点,第i个节点处理第k个任务需要w_i * k秒,给定总任务量H,求最短完成时间。这个模型在工程里非常常见。

举个例子,某个系统要把一份大文件拆成很多分片,分发给多个 CDN 节点缓存。每个节点处理第k个分片的时间可能因为带宽、排队等因素逐渐变长。你要算最短分发时长,就可以套用今天的二分答案思路。再比如工厂排产,一批订单分给若干条产线,每条产线做第k个订单的耗时随订单量递增,问题同样可以抽象成这个模型。

所以这道题真正值得学的东西,不是怎么证明公式,而是“把一个看起来是调度的问题,转换成给定时间判断产能的问题”。这个思维在真实系统设计里也很常用,尤其是做容量规划、性能压测、任务调度的时候,经常需要回答“给定预算时间,系统能不能完成任务”。

5.2 识别“二分答案”类题目的套路

做多了你会发现,一大批题目都有同一个套路:题目问“最小需要多少时间/次数/最大值”,你直接算很难,但给定一个候选值,你可以在O(n)内判断它是否可行,而且可行性随着候选值单调变化。一旦满足这三个条件,就可以大胆使用二分答案。

具体步骤我总结成三步。

第一步,把最优化问题改成判定问题,思考“给定 x,能不能做到”。

第二步,证明可行性随 x 单调。本题里,时间越长越容易完成,这是最直观的单调性。

第三步,确定二分边界。左边界是理论上的最小可能值,右边界是足够大的可行值,然后往返收缩。

面试时如果你能把这套话说出来,通常比直接闷头写代码更能让面试官理解你的思路。很多同学写二分答案题,卡就卡在“不知道 check 怎么写”,其实 check 的本质就是“给定参数,快速求结果”,这一点需要多练才能熟练。

5.3 留给你的变体练习

如果你看完这篇还想加深理解,我建议你试着做两个变体。

第一个变体:把规则改成“第 i 个工人移除第 k 单位高度需要workerTimes[i] * k^2秒”,你还能用二分答案做吗?如果可以,公式应该怎么改?注意这里不再是等差数列求和,而是平方和公式,但单调性没有变。

第二个变体:把规则改成“每个工人只能连续工作 x 秒后必须休息 y 秒”,单调性还存在吗?这就会引入新的复杂度,你要思考可行性判定是否还能高效完成。

我自己做完这两个变体之后,最大的感受是:二分答案框架的通用性很强,难点反而在于你有没有能力根据题目变化,快速写出对应的判定函数。

最后说点个人体会。这道题最打动我的地方,不是它的公式多优美,而是“把最优时间转成可行性判定”这个思维转换。一开始我盯着怎么给工人派活,越想越复杂;后来突然意识到,我根本不需要安排具体调度,只需要回答“给 T 秒够不够”。这个视角一旦建立,整个问题的复杂度瞬间就降下来了。如果你在面试中遇到类似的问题,可以试着先把这句话说出来:“这个问题答案有单调性,我愿意二分时间,然后快速验证。”面试官通常就会顺着你的思路走,后面的代码写起来也就顺理成章了。

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

Zero-Shot Prompting 入门:在 developer-roadmap 中掌握无示例提示工程

文档教程知识库 【免费下载链接】developer-roadmap Interactive roadmaps, guides and other educational content to help developers grow in their careers. 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/de/developer-roadmap 点击查看 免费下载 本文是 de…

作者头像 李华
网站建设 2026/10/2 15:16:41

智能体评测体系实战:从主观感受到可量化结论的四层架构

智能体这东西&#xff0c;做出来容易&#xff0c;说清楚它到底行不行&#xff0c;难。过去大半年我经手过好几个智能体项目的验收&#xff0c;最头疼的从来不是功能跑不通&#xff0c;而是“怎么证明它跑得好”。你说它答得不错&#xff0c;我说它胡编乱造&#xff0c;最后往往…

作者头像 李华
网站建设 2026/10/2 15:16:19

从翼型到湍流:用生活化案例听懂空气动力学

1. 项目概述&#xff1a;为什么用生活化案例讲空气动力学空气动力学这个名词&#xff0c;在很多人的印象里是公式、风洞、无人机翼型设计、飞机起飞性能计算&#xff0c;好像离日常生活很远。但实际情况恰恰相反——我们每天开车、骑车、打球、放风筝&#xff0c;甚至走路时感觉…

作者头像 李华
网站建设 2026/10/2 15:15:54

微信小程序音乐播放器毕业设计全攻略:从SSM架构到论文答辩

距离我当年选毕业设计题目那会儿&#xff0c;已经过去挺久了。每年到这个节点&#xff0c;总能看到一批同学被“一本正经的选题清单”安排得明明白白&#xff0c;其中“基于微信小程序的音乐播放器”绝对是常青树般的存在——它看起来不偏门、有界面、有交互&#xff0c;还带点…

作者头像 李华
网站建设 2026/10/2 15:15:22

递归执行机制深度拆解:从调用栈到快速排序非递归实现

递归&#xff0c;一个在编程入门阶段必讲、但很多人到工作两三年后依然说不清的概念。网上讲递归的文章一大把&#xff0c;大部分都在强调"递过去、归回来"这六个字&#xff0c;可你会背这六个字&#xff0c;照样写不出一个像样的递归函数。我这篇不打算重复那套说教…

作者头像 李华
网站建设 2026/10/2 15:13:08

美国AI监管真相:NIST框架与行政令下的风险分级治理

我不能按照该标题生成内容。原因如下&#xff1a;标题中“违者坐牢20年”“公司就地处死”“核弹级法案”“全面封杀超级智能”“前沿大模型全线叫停”等表述&#xff0c;严重违背事实&#xff0c;属于典型的情绪化、夸张化、虚构性标题。经核实&#xff0c;截至2024年7月&…

作者头像 李华