[HNOI2001] 产品加工

GitHub跳转原题关系图返回列表

把每个任务的选择压成 A 机器总时间这一维,设 dp[x] 表示 A 用时为 x 时 B 的最小用时,最后在所有状态里取 max(A,B) 的最小值。

OJ: luogu

题目 ID: P2224

难度:普及+/提高

标签:动态规划背包状态设计分类讨论

日期: 2026-06-21 09:30

题意

n 个任务,每个任务有三种可能的加工方式:

  • 只用 A 机器,耗时 t1
  • 只用 B 机器,耗时 t2
  • A、B 两台机器同时加工,耗时 t3

其中题面里的 0 表示这种加工方式不可用,不是“耗时为 0”。

要求给每个任务选一种可用方式,使完成全部任务所需的总时间最少。

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 25;
const int INF = 1e9;

int n;
int t1[MAXN], t2[MAXN], t3[MAXN];
int answer;

void dfs(int idx, int sum_a, int sum_b) {
    if (idx > n) {
        answer = min(answer, max(sum_a, sum_b));
        return;
    }

    if (t1[idx] > 0) {
        dfs(idx + 1, sum_a + t1[idx], sum_b);
    }
    if (t2[idx] > 0) {
        dfs(idx + 1, sum_a, sum_b + t2[idx]);
    }
    if (t3[idx] > 0) {
        dfs(idx + 1, sum_a + t3[idx], sum_b + t3[idx]);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
    // 每个任务枚举三种可用加工方式:
    // 只给 A、只给 B、或者 A/B 同时做。
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> t1[i] >> t2[i] >> t3[i];
    }

    answer = INF;
    dfs(1, 0, 0);
    cout << answer << '\n';
    return 0;
}

暴力做法就是对每个任务枚举三种可用模式,最后统计:

  • A 机器总用时 sumA
  • B 机器总用时 sumB

由于两台机器可以并行工作,所以全部任务完成所需的总时间就是:

max(sumA, sumB)

于是题目变成:

给每个任务选一种模式,使 max(sumA, sumB) 最小。

这是一个很典型的“双机负载平衡”模型。
我们只保留一维状态即可。

设:

dp[x] = A 机器总用时恰好为 x 时,B 机器总用时的最小值

处理每个任务时有三种转移:

  1. 只给 A 做:x -> x + t1
  2. 只给 B 做:dp[x] + t2
  3. A、B 一起做:x -> x + t3,同时 dp[x] + t3

第三种方式会同时增加两台机器的负载,这是这题最关键也最容易漏掉的地方。

所有任务处理完后,枚举每个 x,答案就是:

min max(x, dp[x])

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 6005;
const int MAXS = 30005;
const int INF = 1e9;

int n;
int t1[MAXN], t2[MAXN], t3[MAXN];
int dp[MAXS], ndp[MAXS];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    int sum_limit = 0;
    for (int i = 1; i <= n; i++) {
        cin >> t1[i] >> t2[i] >> t3[i];
        sum_limit += max(t1[i], t3[i]);
    }

    for (int i = 0; i <= sum_limit; i++) {
        dp[i] = INF;
    }
    dp[0] = 0;

    int cur_limit = 0;

    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= sum_limit; j++) {
            ndp[j] = INF;
        }

        for (int a_time = 0; a_time <= cur_limit; a_time++) {
            if (dp[a_time] == INF) {
                continue;
            }

            // 当前任务只由 A 机器完成。
            if (t1[i] > 0) {
                ndp[a_time + t1[i]] = min(ndp[a_time + t1[i]], dp[a_time]);
            }

            // 当前任务只由 B 机器完成。
            if (t2[i] > 0) {
                ndp[a_time] = min(ndp[a_time], dp[a_time] + t2[i]);
            }

            // 当前任务由 A、B 两台机器共同完成。
            if (t3[i] > 0) {
                ndp[a_time + t3[i]] = min(ndp[a_time + t3[i]], dp[a_time] + t3[i]);
            }
        }

        cur_limit += max(t1[i], t3[i]);
        for (int j = 0; j <= cur_limit; j++) {
            dp[j] = ndp[j];
        }
    }

    int answer = INF;
    for (int a_time = 0; a_time <= cur_limit; a_time++) {
        if (dp[a_time] == INF) {
            continue;
        }
        answer = min(answer, max(a_time, dp[a_time]));
    }

    cout << answer << '\n';
    return 0;
}

复杂度

S = sum(max(t1_i, t3_i))

  • 时间复杂度 O(nS)O(nS)
  • 空间复杂度 O(S)O(S)

因为 t1,t2,t3 <= 5,所以 S <= 30000,可以通过。

总结

这题看上去像调度题,但本质不是“排顺序”,而是“选模式”。

一旦把每个任务对两台机器的贡献看成:

  • 只加到 A
  • 只加到 B
  • 同时加到 A 和 B

就能自然写出一维负载 DP。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析