二分答案:最早什么时候够用?
不在数组里找数字,而在时间轴上找第一个可行答案。
● 已完成○ 尚未完成◌ 圆环:下一件进度
机器并行、从 0 秒开工;只计算完整产品。连续动画,整数二分。可行色带是教学参考答案,算法只查询 mid。
实验范围:1–4 台,耗时 1–12 秒整数,需求 0–24 件。输入改好后按“应用实验”;非法输入不改变当前有效实验。
看完图,带走这些结论
二分答案把优化问题改成判定问题:不直接猜最优答案,而是问某个候选值是否可行。如果真假结果只有一次分界,就能每次排除一半候选范围,找到第一个可行值。这里搜索的是完成需求的最早整数秒数,不是数组里某个值的下标。
适用条件与边界
机器从 0 秒同时开始、互不干扰,每台生产一件需要固定的正整数秒;只计算完整产品。需求是非负整数,机器至少一台。实验限制 1–4 台、每台 1–12 秒、需求 0–24 件,确保 JavaScript 整数精确且图形可读。一般算法不受此图形范围限制,但必须重新验证整数上界与溢出。
始终保持的规律
最早可行时间始终在 [lo,hi] 内。hi 本身可行;lo 可以可行,也可以不可行。每次比较后缩小区间,直到 lo=hi。
逐步例子
- 先确定搜索空间:候选值 t 是经过的整数秒。每台耗时 kᵢ,t 秒内完成 floor(t / kᵢ) 件;总量 C(t) 是各台之和。enough(t) 判断 C(t) ≥ target。
- 说明单调性:时间增加时,每台已完成的件数都不会减少,所以 C(t) 非下降。某时刻已经够用,所有更晚时刻都够用;某时刻不够,所有更早时刻都不够。
- 用最快机器独自完成全部需求给出上界 U = min(kᵢ) × target。设 lo=0、hi=U,保持最早可行答案位于闭区间 [lo, hi];这里不要求 lo 不可行。
- 取 mid=lo+floor((hi-lo)/2)。若够用,hi=mid,保留这个可能最早的答案;否则 lo=mid+1,因为 mid 及更早都不行。lo<hi 时窗口严格缩小;相等时返回 lo。
- 示例机器耗时 [2,3,7],需求 8 件,上界 16。检查 8 秒:4+2+1=7,不够,区间变 [9,16];检查 12 秒:6+4+1=11,够,变 [9,12];检查 10 秒:5+3+1=9,够,变 [9,10];检查 9 秒:4+3+1=8,够,合拢到 9。
- 确认最优性:9 秒完成 8 件,8 秒只有 7 件,因此最早是 9 秒。若需求为 0,答案直接是 0;两台同为 4 秒、需求 7 件时,12 秒只有 6 件,16 秒有 8 件,答案为 16。
复杂度与整数边界
n 台机器,上界 U=min(kᵢ)×target。每次判定 O(n),二分 O(log(U+1)) 次,总时间 O(n log(U+1)),算法额外空间 O(1)(不含输入)。需求为 0 可直接返回 0。图形为探索时间预计算参考边界、保存步骤和画产量阶梯,有独立展示开销,不能当作二分运行时间。C++ 示例的 kᵢ、target 至多 10⁹,上界至多 10¹⁸,用 long long;累计产量提前截断,防止溢出。
常见误区
- 把要搜的时间误当成机器数组下标。机器耗时不需要排序;这里依赖的是 enough(t) 的单调性。
- 看见 true 就立即返回。它只说明这个时间够用,不代表最早;要令 hi=mid 继续向左找。
- 把这里的 hi=mid 与原二分查找的 hi=mid-1 混用。查第一个 true 与查相等元素维护的目标不同;循环条件、区间含义和更新必须成套。
- 没有证明单调性就套模板。[F,T,F,F,T,T,T,T] 在中点看到 F 并不能排除左边的 T。
- 把尚未做完的产品算成件数,或让动画插值改变算法状态。产量用向下取整;画面可连续变化,但二分只检查整数秒。
- 直接累加所有机器在大上界下的产量可能溢出。判定只需要知道是否够用,可在达到目标前提前返回;零耗时会除零,必须拒绝。
- 没有真实数组,也能用标准库:C++20 的 std::views::iota(0LL, U + 1LL) 表示候选整数 [0,U],无需预存数组;这里 U 为 long long,且 0≤U≤10¹⁸。std::ranges::partition_point 要求谓词先 true 后 false(两段均可为空),所以本课传入 !enough(t)。先判断返回迭代器是否等于 end:相等表示该候选区间内无解,否则才把 *it 用作答案。仍须证明单调性、避免判定溢出,并确保排他上界 U+1 可表示。本课显式写循环是为了展示区间不变量。
自测与答案
两台机器都需要 4 秒一件,需求 7 件。12 秒够吗?最早多少秒?如果需求为 0 呢?
12 秒每台 3 件,总共 6 件,不够。16 秒每台 4 件,总共 8 件,最早 16 秒。需求为 0 时,最早 0 秒;无需启动生产。
可选进阶:固定其他参数,寻找最后一个可行公差
2026-10-17结束的AtCoder ABC480 E要求统计:长度至少3、首项与公差均为正整数、所有项的乘积不超过N的等差数列。先固定首项a和长度m,只让公差d变化。各项为a、a+d、…、a+(m−1)d;d增大时每项都不减且后面的项严格增大,所以乘积严格增大。谓词‘乘积≤N’先true后false,我们要找最后一个true,与上方机器实验寻找第一个true的方向不同。
例如固定N=120、a=1、m=3:d=7时是1、8、15,乘积120刚好可行;d=8时是1、9、17,乘积153不可行。由单调性,正公差1到7全可行,这组首项和长度贡献7种数列;它不是整道题的总数18。只要固定参数下d=1就不行,该组贡献便是0。
对原题N≤10¹⁸,可用lo=0作‘尚无正公差解’的哨兵、hi=10⁹作不可行哨兵;hi处仅前三项的乘积就至少为1×(1+10⁹)×(1+2×10⁹)>10¹⁸。保持正公差答案在lo及hi之间的分界:hi−lo>1时取下中点,若可行令lo=mid,否则hi=mid;结束返回lo。lo=0不是真实允许的公差,也不要求代入谓词成立。不能照搬机器实验里“可行则hi=mid”的分支,更不能用lo=mid却保留lo<hi的循环条件。
不用先算巨大乘积:令q=N,依次对每个正整数项v做q=floor(q/v),最后q>0恰好等价于所有项的乘积≤N。这利用连续向下取整的整数除法等于对总乘积向下取整;中途q变0后可直接判不可行。例中120÷1÷8÷15依次向下取整得1,120÷1÷9÷17则得0。整数项的生成和N本身也必须精确;JavaScript原题范围应使用BigInt,不能先用Number算完再转换。
完整计数还要枚举首项和长度:a³≤N给出a≤10⁶,最小长度m的正整数递增数列乘积至少m!,而20!>10¹⁸,所以只需m=3到19。每组固定参数的最大可行公差就是这一组的数量,再求和;这一页只解释二分判定的迁移,不把单次二分冒充整题算法。上方SVG、交互和同步代码仍只演示机器生产时间,不模拟这些大整数等差数列。
延伸学习
换一个搜索空间:在有序 tails 中找第一个 ≥ x 或 > x 的位置。若不存在就返回 end 并追加,而不是返回一个可解引用的元素;这与本课已有可行 hi 的前提不同。
参考资料
enough:尽早返回,避免累计溢出
下例适用 1 ≤ n ≤ 200000、1 ≤ kᵢ ≤ 10⁹、0 ≤ target ≤ 10⁹,参数已经校验,机器非空。fastest 与 target 都用 long long,上界最多 10¹⁸。累加前先比较剩余需求,避免把所有机器的产量直接相加。超出这些范围需要重新验证整数表示,不可盲目照搬。
bool enough(long long sec) {
long long made = 0;
for (long long k : periods) {
long long add = sec / k;
if (add >= target - made) return true;
made += add;
}
return made >= target;
}
// fastest = *min_element(periods.begin(), periods.end());
long long lo = 0, hi = fastest * target;
while (lo < hi) {
long long mid = lo + (hi - lo) / 2;
if (enough(mid)) hi = mid;
else lo = mid + 1;
}
return lo; // 第一个可行时刻