两阶段流水车间:用事件小根堆求 A、B 车间各自的最优完工时刻向量,再把 A 完工时刻降序与 B 槽位升序反向配对得到最小总工期。
OJ: roj
题目 ID: 2067
难度:普及+/提高-
标签:贪心堆模拟python
创建: 2026-10-01 06:17
更新: 2026-10-07 13:50
形式化题目
有两组同型并行机:A 型机器单次操作耗时为
记
要求依次输出:
,即 A 车间单独的最短完工时间; - 在允许 A、B 重叠(每个工件各自满足 A 先于 B)的前提下,
,即整个流水线的最短完工时间。
正解
思路
关键观察一:每台机器的时间表就是自己的倍数序列。
给一台耗时
把每台机器当前的下一个键 N 次:
弹出的第
所以第
并且任何分配方案排序后的完工时刻向量,都逐项不小于
下面这张表是样例 A 车间(时长 1, 1,N = 5)的弹出过程,
每一行表示一次「把工件交给键最小的那台机器」:
| 第几次弹出 | 选中机器时长 | 产生完工时刻 | 该机器下次的键 |
|---|---|---|---|
| 1 | 1 | 1 | 2 |
| 2 | 1 | 1 | 2 |
| 3 | 1 | 2 | 3 |
| 4 | 1 | 2 | 3 |
| 5 | 1 | 3 | 4 |
得到
关键观察二:B 车间的最优收尾可以闭式表达。
把 B 各机器的时间表(
-
下界(可行性条件):若总收尾不超过
,则对每个 , 在 之后才到达的工件只能用 之后的槽位: 取
( 降序第 项)即得 ,对每个 成立。 -
充分性:反过来若
,对任意 令 ,则 ,于是 , 上述条件成立,按 由大到小逐层匹配即可排开。
所以在固定的到达序列下
即 A 车间最晚完工的工件配 B 车间最早的槽位。这个表达式对每个
合并两阶段。 设某个方案下 A 的完工向量为 best_B 的单调性,
该方案的收尾时刻至少是
这就给出了下界。上界则由「取观察一的贪心分配(完工向量恰为
这段推理里唯一容易做错的是配对方向:必须让 A 降序 对 B 升序, 因为「最晚出炉的工件应该优先抢占 B 车间最早的槽位」。下面用样例把这一步走一遍。
样例 N=5,A 时长 1, 1(得 3, 1, 4。
B 三台机器的时间表分别是 3,6,…、1,2,3,4,5,…、4,8,…,
归并取前 5 项得 3 出现了两次,分别来自时长 3 的机器
和时长 1 的机器的第三件——
| 和 | |||
|---|---|---|---|
| 1 | 3 | 1 | 4 |
| 2 | 2 | 2 | 4 |
| 3 | 2 | 3 | 5 |
| 4 | 1 | 3 | 4 |
| 5 | 1 | 4 | 5 |
最大值为 3 5 一致。
若误把
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 06:17
# update_at: 2026-10-01 06:17
import heapq
import sys
from collections.abc import Iterator
def finish_times(times: list[int], jobs: int) -> list[int]:
"""把 jobs 个工件派给这批同型机器,按升序返回「全车间完成第 k 个工件」的时刻。
每台机器的工作时间总是 t 的倍数:t, 2t, 3t, ...。把「现在派一个工件给这台机器」
看成一次待选事件,键取该事件产生的完工时刻,则小根堆弹出第 k 次就是第 k 个工件
最早能完工的时刻;得到的升序向量在各派工方案中逐项最小,最后一项即最短完工时间。
"""
heap = [(t, t) for t in times] # (此刻派活的完工时刻, 机器时长)
heapq.heapify(heap)
done: list[int] = []
for _ in range(jobs):
finish, t = heap[0] # 下一次派活完工最早的机器
heapq.heapreplace(heap, (finish + t, t)) # 同一台机器顺延到下一件
done.append(finish)
return done
def solve() -> None:
data: Iterator[int] = iter(map(int, sys.stdin.buffer.read().split()))
n, m1, m2 = next(data), next(data), next(data)
time_a = [next(data) for _ in range(m1)] # A 型机器做一次操作的时间
time_b = [next(data) for _ in range(m2)] # B 型机器做一次操作的时间
finish_a = finish_times(time_a, n) # 升序:A 车间第 k 个完工时刻
finish_b = finish_times(time_b, n) # 升序:B 车间第 k 个可收尾槽位
ans_a = finish_a[-1]
# 慢的先交给 B:A 完工时刻从大到小,对齐 B 从小到大的空闲槽位,最大值最小
ans_b = max(a + b for a, b in zip(reversed(finish_a), finish_b))
print(ans_a, ans_b)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 10:54
* update_at: 2026-10-06 10:54
*/
#include <cstdio>
#include <queue>
#include <utility>
using namespace std;
typedef long long ll;
const int MAXN = 1005; // 工件数量上限
const int MAXM = 35; // 单类机器数量上限
ll n; // 工件数量
ll m1; // A 型机器数量
ll m2; // B 型机器数量
ll time_a[MAXM]; // A 型每台机器完成一次操作的时间
ll time_b[MAXM]; // B 型每台机器完成一次操作的时间
ll finish_a[MAXN]; // A 车间第 k 个工件的最早完工时刻,升序
ll finish_b[MAXN]; // B 车间第 k 个可用收尾槽位,升序
// 把 jobs 个工件派给这批同型机器,升序返回「全车间第 k 个工件的最早完工时刻」。
// 每台机器的时间表是 t,2t,3t,...,把「给这台机器再派一件」看作待选事件,
// 事件键就是它产生的完工时刻;小根堆弹出第 k 次即为第 k 个完工时刻。
void finish_times(ll times[], ll m, ll jobs, ll done[]) {
// 堆元素:(这台机器下一个完工时刻, 该机器单次耗时)
priority_queue<pair<ll, ll>, vector<pair<ll, ll> >, greater<pair<ll, ll> > > heap;
for (ll j = 1; j <= m; j++) {
heap.push(make_pair(times[j], times[j]));
}
for (ll k = 1; k <= jobs; k++) {
ll finish = heap.top().first;
ll t = heap.top().second;
heap.pop();
done[k] = finish;
heap.push(make_pair(finish + t, t)); // 同一台机器顺延到下一件
}
}
int main() {
scanf("%lld %lld %lld", &n, &m1, &m2);
for (ll j = 1; j <= m1; j++) {
scanf("%lld", &time_a[j]);
}
for (ll j = 1; j <= m2; j++) {
scanf("%lld", &time_b[j]);
}
finish_times(time_a, m1, n, finish_a);
finish_times(time_b, m2, n, finish_b);
ll ans_a = finish_a[n]; // A 车间独立完成的最短时间
// B 车间收尾:A 完工时刻降序第 p 项,配 B 升序第 p 个槽位,取最大
ll ans_b = 0;
for (ll p = 1; p <= n; p++) {
ll cur = finish_a[n - p + 1] + finish_b[p];
if (cur > ans_b) {
ans_b = cur;
}
}
printf("%lld %lld\n", ans_a, ans_b);
return 0;
}复杂度
- 时间复杂度:两次事件堆过程各弹出
N次,堆大小不超过机器数, 故为 ; 最后的配对是 。 时是毫秒级。 - 空间复杂度:两个长度
N的完工向量加两个大小的堆, 共 ,远低于 128 MB。
总结
- 本题是两阶段流水车间:缓冲库无限让两个阶段可以解耦,先最优安排 A,再在 A 的完工序列上最优安排 B。
- A 车间的最优完工向量可以用「事件小根堆」直接得到,等价于闭式
; 这个向量在任何方案下都逐项最优,是后续能直接用它算答案的原因。 - B 车间在固定到达序列下的最优收尾是
,其中 是 B 各机器时间表归并后的前 项; 晚完工的工件要配早的槽位。 - 两个阶段的最优解来自同一个分配,所以两问可以一起算出来,总复杂度
。
图示解析
这张图给出从输入到两个答案的主线:
flowchart LR
A["N 个工件 + A 机器时长 / B 机器时长"]
B["A 的时间表 = t, 2t, 3t, ..."]
C["事件小根堆弹 N 次 ⇒ c 升序向量"]
D["ans_A = c_N"]
E["B 的时间表归并前 N 项 ⇒ b 升序"]
F["c 降序 × b 升序 取最大"]
G["ans_B"]
A --> B --> C --> D
A --> E
C --> F
E --> F
F --> G
A、B 两条时间表各自归并成升序向量;A 的向量既直接给出第一问,
又与 B 的向量反向配对给出第二问。读图时注意 F 处汇合,正是「先定 A 的最优向量、再算 B」这一解耦的关键。
