qinghuan
← 回到知识探索

计算机趣闻 / 可下载小项目

三台机器同时生产,最早第几秒够七件?

机器耗时没有排成一张答案表,也能二分:先判断某一时刻够不够,再始终夹住最后一个不够与第一个够用的边界。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先固定生产规则,数字才有明确含义
  4. 第 t 秒完成的件数,为什么要向下取整
  5. 产量会停一会儿,也会一下跳过目标
  6. 左边确定不够,右边确定够用
  7. 七件产品的三次询问,把区间缩成7与8
  8. 让每台机器逐秒倒计时,作为另一份核对
  9. 一次判定遍历机器,搜索只询问对数级的时刻
  10. 记住这些要点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 3 个解释
向下取整
保留不超过商的最大整数,用来只计完整完成的件数。
单调可行判断
条件一旦满足,继续往选定方向就不会重新不满足。
可行边界
已经证实能满足要求的一个位置,用它保留答案可能出现的范围。
打开完整术语小抄 ↗

三台机器做一件产品分别需要3秒、2秒、5秒,从时刻0一起开始。订单要7件。等最快那台独自做完需要14秒,三台一起工作显然可能更早,怎样找到第一个已经够数的时刻?

不必把0秒到答案之间的每一秒都列出来。我们先会回答“到第 t 秒一共完成几件”,再用这个判断缩小时间范围。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:完整可运行源码
  • README.md:运行步骤与练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和运行验证记录
  • SHA256SUMS.txt:文件摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B demo.py

解压后,在含 demo.py 的目录打开终端。命令分别回车执行。独立机器从0同时连续开工,固定正整数秒耗时;780份机器清单、16380个目标对逐秒倒计时,另含2组大整数、12组顺序变化和14类拒绝输入;无真实排产或性能测量。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先判断某个时刻够不够,再找最早的可行时刻

正在准备实验控件…

已知不够的 lo
0
已知够用的 hi
14
本轮检查 mid
7
到 mid 完成总数
6

三台机器分别完成 [2, 3, 1] 件,总数 6 < 目标 7。

当前时刻不够,更早也不会够,所以把 lo 改成 mid。 新边界:lo=7,hi=14。

初始上界 14 秒来自“只让最快机器做完全部目标”;可行条件用 ≥,因为同时产出可能直接越过目标数。

三台机器从 t=0 同时开始,各自每完成一件才计数;耗时为正整数,没有启动成本、停机或共享资源限制。页面只取 1–10 秒和 0–30 件,整数计算可精确表示;Python 下载版另外核对大整数和非法配置。

先固定生产规则,数字才有明确含义

本模型中所有机器从时刻0开始,各自连续生产;一台做完一件后立即开始下一件。单件耗时是固定的正整数秒,第一件在该耗时结束时才完成。机器之间没有共享工位、抢原料或相互等待,也没有启动准备、停机和换型时间。

这些条件让各台产量可以直接相加。我们求订单数量至少达标的最早时刻;未完成的一件不计入产量。让机器主动闲置不会更早达标,因此连续开工给出本模型中最快能完成的数量。

函数接收非空的 list 或 tuple,每项必须是正整数,目标必须是非负整数;布尔值不作为数量或秒数。没有可用机器、耗时为零或负数、浮点耗时等输入都会明确拒绝。先验证配置,再处理目标0,避免错误配置因目标为0而被悄悄接受。

第 t 秒完成的件数,为什么要向下取整

一台每 k 秒完成一件,完成时刻就是 k、2k、3k……。到第 t 秒已经完成几件,就是 t//k。比如一件需3秒,到第7秒只完成2件,第三件还差2秒;不能把7/3得到的部分件数算成完成。

所以总产量为各台 t//k 的和。第7秒,三台分别完成[2,3,1],总共6件;第8秒分别完成[2,4,1],总共7件。这两个相邻时刻已经说明答案是8秒。

全部变量使用整数,代码直接写 //。先做 / 转成浮点,再转回 int,会把大整数问题变成浮点精度问题;而且我们需要的数学含义本来就是完整件数。

产量会停一会儿,也会一下跳过目标

时间增加时,完成的产品不会减少。因此“总产量至少达到目标”的判断,只会从不满足变成满足,之后保持满足。二分依赖的是这个真假顺序,不要求机器耗时列表先排序,也不要求每一秒产量都增加。

两台机器都每2秒完成一件,产量会从0跳到2,再跳到4。目标若是3件,永远不会遇到总量恰好等于3的时刻;但第4秒已经够用。因此判定必须写 completed >= target,不能只接受等号。

这里搜索的对象是整数时间,由计算规则隐式给出产量。我们没有创建一个包含所有时刻的数组;每次只计算正在询问的那一秒。

左边确定不够,右边确定够用

正目标下,lo=0 一定不够,因为还没有完成任何产品。令 hi=最快单件耗时×目标;就算只有最快机器开工,到这个时刻也能做够,其他机器只会增加数量,所以 hi 一定够用。

循环始终保持 lo 不可行、hi 可行。取中点 mid:如果够用,就把 hi 改为 mid,让它继续作为可能的最早答案;如果不够,就把 lo 改为 mid,因为更早的时刻也都不够。

当 hi−lo=1,两端之间没有别的整数时刻了,返回 hi。目标0在验证输入后直接返回0,不进入这个需要“lo一定不够”的循环。最早完成时刻也是整数,因为本模型所有机器的完成事件都发生在整数秒。

七件产品的三次询问,把区间缩成7与8

三台耗时[3,2,5],目标7,初始两端为0与14。先问第7秒,产量6件不够,所以 lo=7;现在答案仍在7秒之后、14秒以内。

再问第10秒,产量为3+5+2=10件,已经够用,于是 hi=10。然后问第8秒,产量为2+4+1=7件,也够用,于是 hi=8。此时 lo=7、hi=8,相邻两端一边不够、一边够用,答案确定。

边界定义和更新公式是一套约定。若把可行的 mid 随手改成 hi=mid−1,却仍声称 hi 始终可行,就可能把真正答案排除或破坏证明。修改循环写法时,要重新检查每个边界代表什么。

让每台机器逐秒倒计时,作为另一份核对

保存完整源码为 demo.py,执行 python demo.py。2026-10-23 在 Windows、CPython 3.14.0 实跑,只用标准库。输出包含默认二分轨迹、两台同速机器跳过目标的例子、目标0和拒绝输入统计。

ticking_oracle 给每台机器一个剩余秒数,每过一秒减一,减到零就记一件并重新装入该机器的单件耗时。它逐秒模拟,不使用二分,也不调用 t//k 的产量公式,适合在小输入上独立核对最早完成时刻。

程序枚举1到4台机器、每台耗时1到5秒的780份清单,对目标0到20逐个检查,共16,380个结果与倒计时一致。另检查两组大整数、12组机器顺序变化和14类非法输入;错误输入在进入搜索前拒绝,检查使用显式 AssertionError。

展开收起代码 · 小实验python · 115 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Independent machines start at t=0; positive integer seconds per item."""
from itertools import product


def require(condition, message):
    if not condition:
        raise AssertionError(message)


def min_time(periods, target, emit=None):
    """Earliest integer time when total completed items >= target.

    periods: nonempty list/tuple of positive int values, excluding bool.
    target: nonnegative int, excluding bool. Validate even when target is zero.
    Inputs are not changed; observers must not mutate periods.
    """
    if not isinstance(periods, (list, tuple)) or not periods:
        raise ValueError("a nonempty list or tuple of machine periods is required")
    if any(type(k) is not int or k <= 0 for k in periods):
        raise ValueError("each period must be a positive int, not bool")
    if type(target) is not int or target < 0:
        raise ValueError("target must be a nonnegative int, not bool")
    if target == 0:
        return 0

    lo = 0  # Infeasible: positive target, zero completed items.
    hi = min(periods) * target  # Feasible: the fastest machine alone suffices.
    while hi - lo > 1:
        mid = lo + (hi - lo) // 2
        completed = sum(mid // k for k in periods)
        if emit is not None:
            emit(lo, hi, mid, completed)
        if completed >= target:
            hi = mid  # This works, but an earlier time may also work.
        else:
            lo = mid  # This and all earlier times cannot work.
    return hi


def ticking_oracle(periods, target):
    """Small-test reference: advance each machine's countdown one second.

    No binary search and no time // period production formula are used here.
    Only called with known-valid small inputs.
    """
    remaining = list(periods)
    completed = seconds = 0
    while completed < target:
        seconds += 1
        for index, period in enumerate(periods):
            remaining[index] -= 1
            if remaining[index] == 0:
                completed += 1
                remaining[index] = period
    return seconds


def main():
    periods, target = [3, 2, 5], 7
    trace = []
    answer = min_time(periods, target, lambda lo, hi, mid, count: trace.append((lo, hi, mid, count)))
    require(answer == 8 and ticking_oracle(periods, target) == 8, "official model example")
    print(f"periods={periods}; target={target}; guaranteed_upper={min(periods) * target}")
    for lo, hi, mid, count in trace:
        print(f"lo={lo}; hi={hi}; mid={mid}; completed={count}; feasible={count >= target}")
    print(f"answer={answer}; at_7={[7 // k for k in periods]}; at_8={[8 // k for k in periods]}")
    jump = min_time([2, 2], 3)
    require(jump == 4, "must accept >= target when count jumps over it")
    require(min_time([3, 2, 5], 0) == 0, "zero items")
    print(f"jump_periods=[2, 2]; target=3; answer={jump}; completed=4; zero_target=0")

    machine_lists = cases = 0
    for n in range(1, 5):
        for periods in product(range(1, 6), repeat=n):
            machine_lists += 1
            previous = -1
            for target in range(21):
                answer = min_time(periods, target)
                wanted = ticking_oracle(periods, target)
                require(answer == wanted, f"oracle mismatch: {periods}, {target}")
                require(answer >= previous, "more requested items finished earlier")
                previous = answer
                cases += 1
    require(machine_lists == 780 and cases == 16380, "enumeration coverage")

    large_target = 10**40
    require(min_time([7], large_target) == 7 * large_target, "large single-machine integer")
    period, target = 10**30 + 7, 2 * 10**20 + 1
    require(min_time([period, period], target) == period * ((target + 1) // 2), "large simultaneous events")
    order_cases = 0
    for periods in ([3, 2, 5], [1, 5, 3, 2], [5, 5, 2, 1]):
        before = list(periods)
        for target in (0, 1, 7, 20):
            require(min_time(periods, target) == min_time(periods[::-1], target), "machine order matters")
            order_cases += 1
        require(periods == before, "input mutated")

    invalid = [([], 0), ([], 1), ([0], 1), ([-2], 1), ([1.5], 1), ([True], 1),
               (None, 1), ("23", 1), (iter([2, 3]), 1), ([[2]], 1),
               ([2], -1), ([2], True), ([2], 1.0), ([2], "7")]
    rejected = 0
    for periods, target in invalid:
        try:
            min_time(periods, target)
        except ValueError:
            rejected += 1
        else:
            raise AssertionError("invalid input accepted")
    print(f"machine_lists={machine_lists}; target_cases={cases}; huge_integer_cases=2; order_cases={order_cases}")
    print(f"rejected_inputs={rejected}; validates_configuration_before_zero_target=True")
    print("PASS: minimum times match ticking oracle; model has no startup/shared-resource limits")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。独立机器从0同时连续开工,固定正整数秒耗时;780份机器清单、16380个目标对逐秒倒计时,另含2组大整数、12组顺序变化和14类拒绝输入;无真实排产或性能测量。

运行后对照结果
periods=[3, 2, 5]; target=7; guaranteed_upper=14
lo=0; hi=14; mid=7; completed=6; feasible=False
lo=7; hi=14; mid=10; completed=10; feasible=True
lo=7; hi=10; mid=8; completed=7; feasible=True
answer=8; at_7=[2, 3, 1]; at_8=[2, 4, 1]
jump_periods=[2, 2]; target=3; answer=4; completed=4; zero_target=0
machine_lists=780; target_cases=16380; huge_integer_cases=2; order_cases=12
rejected_inputs=14; validates_configuration_before_zero_target=True
PASS: minimum times match ticking oracle; model has no startup/shared-resource limits

一次判定遍历机器,搜索只询问对数级的时刻

设机器数为 n,初始可行上界为 U。一次产量计算遍历 n 台机器;每轮把时间范围大约减半,正目标的总成本按整数操作次数计为 O(n log(U+1)),包含最初的输入检查。核心只保存固定数量的计数与边界;若把全部轨迹保存下来,显示所需空间另外计。

Python 整数可以扩展位数,但大整数乘除并不是固定成本;上述写法统计的是操作次数,没有声称任意长度整数都在相同时间内完成。移植到固定宽度语言时,还要检查上界乘法、总产量累加与中点计算的溢出。

本模型没有测量真实车间吞吐,也没有求共享资源下的排产。若机器有启动时间、停机、原料限制或彼此等待,要先重新建立产量与可行性判断,并确认它仍符合要搜索的边界;只保留二分循环,不能自动解决改变后的业务问题。

记住这些要点

  • 每台完成件数是 t//k,总量按独立机器相加。
  • 目标是至少够用,产量可以直接跳过目标。
  • 始终保持 lo 不可行、hi 可行,相邻时返回 hi。
  • 目标0先单独处理;模型规则和每次判定的成本都要写清。

别把例子用过头

  • 只接受产量恰好等于目标,漏掉同时完成多件的时刻。
  • 错误地要求机器耗时先排序,忽略真正单调的是时间上的可行判断。
  • 没有已证明可行的上界,或混用另一种二分的边界更新。
  • 允许0秒耗时导致除零,或把模型估算当作真实排产测量。

自己试一小步

不展开所有时间,怎样证明你找到的那个时刻确实是最早?

  1. 自选三台正整数耗时,目标不要刚好被每次产量增长整齐命中。
  2. 手写一个确定不够与一个确定够用的时刻,再逐轮列出lo、hi、mid和产量。
  3. 对返回时刻及前一秒分别核对;另测目标0、单台机器与两台同速机器。
  4. 把模型改为“其中一台启动前要等待”,只写出原判定哪里需要改变,不先假定原程序仍然正确。

做到这些,就算完成

  • 每轮都能解释左右边界的真假含义。
  • 至少一例产量跨过目标,仍返回最早够用的时刻。
  • 独立核对方法不直接调用同一个二分或产量计算函数。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • CSES:Factory Machines (新标签页)↗CSES Problem Set · 英文

    独立机器并行制造产品的任务定义,以及单件耗时3、2、5秒、目标7件时答案为8秒的例子。

    2026-10-23 实际打开并阅读:完整题面、输入约束和样例;原题目标至少1,本文另行定义并验证目标0的行为;仅声明此范围,不代表通读整部资料。

  • Competitive Programmer's Handbook:Finding the smallest solution (新标签页)↗Antti Laaksonen / 作者 cphb 仓库 · 英文

    可行判断只发生一次真假转折时,可以搜索最早可行位置;判定代价应计入总成本。

    2026-10-23 实际打开并阅读:Finding the smallest solution 小节全部正文、起始上界条件与判定次数分析;本文使用另一种边界写法;仅声明此范围,不代表通读整部资料。

  • Python 3.14:整数整除 (新标签页)↗Python Software Foundation · 英文

    整数//得到向下取整的整数商,避免先转浮点再取整。

    2026-10-23 实际打开并阅读:6.7 Binary arithmetic operations 的 /、//、% 及其整数关系说明;仅声明此范围,不代表通读整部资料。