交换求和次序把启动费一次记在批上,分批 DP 的转移只依赖前缀和,O(N^2) 求出最小总费用。
OJ: roj
题目 ID: 1606
难度:普及+/提高-
标签:动态规划前缀和python
创建: 2026-09-30 21:27
更新: 2026-10-07 13:50
形式化题目
形式化地:设划分点
样例的最优方案是
正解
思路
朴素想法与瓶颈。 最自然的 DP 是设
下图展示样例三条批的时间线,观察完成时刻如何逐批累加、前批的耗时被后批任务共享:
flowchart LR b1["批 1,2<br/>S+1+3 = 5"] --> b2["批 3<br/>S+4 = 5"] --> b3["批 4,5<br/>S+2+1 = 4"] f1["完成时刻 5"] -.-> b1 f2["完成时刻 10"] -.-> b2 f3["完成时刻 14"] -.-> b3
批 1 的
换一种记账方式。 把总费用按批分组(交换求和次序):批
批
DP 定义。 用前缀和
下表是样例(
| 行 |
0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|---|
| 1 | 30 | — | — | — | — | 30( |
| 2 | 75 | 78 | — | — | — | 75( |
| 3 | 135 | 126 | 125 | — | — | 125( |
| 4 | 165 | 150 | 145 | 146 | — | 145( |
| 5 | 180 | 162 | 155 | 153 | 153 | 153( |
注意
代码
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-09-30 21:29
# update_at: 2026-09-30 21:29
import sys
from itertools import accumulate
def min_cost(startup: int, prefix_t: list[int], prefix_c: list[int]) -> int:
"""前 n 个任务分批的最小总费用 f[n](f[0] = 0,O(n^2))。
同一批任务同时完成,所以批 (j, i] 的启动费 startup 会推迟它自己以及后面
所有批次里任务的完成时刻;把总费用按批次交换求和次序,批 (j, i] 的贡献就是
(startup + 该批时间和) × (j 之后所有任务的费用系数和),前缀和让两项都是 O(1)。
"""
total_c = prefix_c[-1]
n = len(prefix_t) - 1
f = [0] * (n + 1) # f[i]:前 i 个任务分批的最小总费用
for i in range(1, n + 1):
time_i = prefix_t[i]
f[i] = min(
f[j] + (startup + time_i - prefix_t[j]) * (total_c - prefix_c[j])
for j in range(i)
)
return f[n]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, startup = next(data), next(data) # 任务数 n、每批启动时间 S
tasks = [(next(data), next(data)) for _ in range(n)] # 按题面结构成对消费:每个任务一行 (T_i, C_i)
times = [t for t, _ in tasks] # 各任务耗时 T_i,n 个
fees = [c for _, c in tasks] # 各任务费用系数 C_i,n 个
prefix_t = list(accumulate(times, initial=0)) # n+1 个前缀和,下标 0 处为 0
prefix_c = list(accumulate(fees, initial=0))
print(min_cost(startup, prefix_t, prefix_c))
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-05 12:23
* update_at: 2026-10-05 12:23
*
* 「一本通 5.6 例 1」任务安排 1
* 顺序不可变的序列切成连续若干批,每批开始前有固定启动时间 S。
* 一批的耗时会被它之后所有任务共享,直接按“完成时刻”DP 会失去无后效性;
* 交换求和次序后,批 (j, i] 的贡献 = (S + 该批耗时和) × (j 之后所有 C 之和),
* 两项都只依赖前缀和,于是 f[i] 只需枚举切点 j 即可转移:
* f[i] = min_{j<i} f[j] + (S + preT[i] - preT[j]) * (preC[N] - preC[j])。
* 时间复杂度 O(N^2),空间 O(N)。
*/
#include <cstdio>
using namespace std;
typedef long long ll;
const int MAXN = 5005;
const ll INF = 1000000000000000000LL; // 取最小值用的哨兵,远大于真实答案上界
ll n, S;
ll preT[MAXN]; // preT[i] = 前 i 个任务的耗时前缀和
ll preC[MAXN]; // preC[i] = 前 i 个任务的费用系数前缀和
ll f[MAXN]; // f[i] = 前 i 个任务分批的最小总费用(按批记账)
int main() {
scanf("%lld %lld", &n, &S);
for (ll i = 1; i <= n; i++) {
ll t, c;
scanf("%lld %lld", &t, &c);
preT[i] = preT[i - 1] + t;
preC[i] = preC[i - 1] + c;
}
ll totalC = preC[n]; // 全部任务的费用系数之和,用于取后缀和 preC[N] - preC[j]
f[0] = 0;
for (ll i = 1; i <= n; i++) {
ll best = INF;
// 枚举最后一批的起点 j+1,即切点 j 把前 j 个任务作为已处理前缀
for (ll j = 0; j < i; j++) {
ll cand = f[j] + (S + preT[i] - preT[j]) * (totalC - preC[j]);
if (cand < best)
best = cand;
}
f[i] = best;
}
printf("%lld\n", f[n]);
return 0;
}复杂度
- 时间复杂度:前缀和
,DP 双层枚举 ; 时约 次 转移,与标准 C++ 解法同阶。 - 空间复杂度:两条前缀和 + 一个
f数组,。 - 费用可达约
,超过 32 位整数;Python int任意精度,无需额外处理。
总结
- 顺序固定 + 连续分批,是标准的区间划分 DP 结构;难点只在启动费
的跨段耦合。 - 用交换求和次序把"每批影响未来"改写成"每批乘以它之后的
后缀和",耦合在记账层面消失,转移量全部前缀和化。 - 朴素的"完成时刻入状态"不可行,是因为完成时刻依赖前缀的具体划分而非仅依赖
;换记账后状态只需 这一个切点。 即为本题正解量级, 下不需要斜率优化。