货物调度

按仓库取价值最高的前 k 件货物,将每个仓库视为多重选择组,再以总费用为容量做背包。

OJ: shumeng

题目 ID: CSP202406D

难度:提高+/省选-

标签:背包分组背包排序

日期: 2026-07-31 16:21

形式化题目

nn 个仓库,仓库 jj 卖出货物要支付基本运费 bjb_j 和每件 cjc_j 的计件运费;仓库中放有若干货物,每件价值 aia_i。可以选择卖任意数量的货物,只要某仓库卖 kk 件,就要付 bj+kcjb_j + k c_j

求满足“卖出的货物总价值 - 总费用 v\ge v”的前提下,最小的总费用。

思路

先观察一个简化事实:同一仓库决定卖 kk 件时,计件费用固定,为了净现金最大应取价值最高的 kk 件。于是每个仓库的决策从“选哪些货物”压缩成“选前 kk 件”,问题变成分组背包。

朴素做法:01 序列枚举每件货物

先看最直接的做法,用 01 选择序列枚举每件货物卖或不卖,到叶子节点统一检查方案是否满足目标。

cpp
/**
 * 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-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
// brute.cpp:小数据暴力解,用 01 选择序列枚举每件货物卖或不卖,只适合 m 很小的数据。
#include <bits/stdc++.h>
using namespace std;

int n, m, target;
vector<int> basic_cost, piece_cost, value, warehouse;
vector<int> choose_item; // choose_item[i] 表示第 i 件货物:0 不卖,1 卖
int answer;

// 检查当前完整的 01 选择序列对应的方案是否满足目标
void check_choice() {
    vector<int> count(n, 0);
    vector<int> total_value(n, 0);
    for (int i = 0; i < m; i++) {
        if (choose_item[i] == 0) continue;
        count[warehouse[i]]++;
        total_value[warehouse[i]] += value[i];
    }
    int profit = 0;
    int cost = 0;
    for (int i = 0; i < n; i++) {
        if (count[i] == 0) continue;
        cost += basic_cost[i] + count[i] * piece_cost[i];
        profit += total_value[i] - basic_cost[i] - count[i] * piece_cost[i];
    }
    if (profit >= target) answer = min(answer, cost);
}

// 第 index 件货物只有卖/不卖两种选择
void enumerate_choice(int index) {
    if (index == m) {
        check_choice();
        return;
    }
    choose_item[index] = 0;
    enumerate_choice(index + 1);
    choose_item[index] = 1;
    enumerate_choice(index + 1);
}

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

    cin >> n >> m >> target;
    basic_cost.resize(n);
    piece_cost.resize(n);
    for (int i = 0; i < n; i++) cin >> basic_cost[i] >> piece_cost[i];
    value.resize(m);
    warehouse.resize(m);
    for (int i = 0; i < m; i++) cin >> value[i] >> warehouse[i];

    choose_item.assign(m, 0);
    answer = 1000000000;
    enumerate_choice(0);
    cout << answer << '\n';
    return 0;
}

枚举所有 2m2^m 个子集,mm 稍大就不可行,只适合小数据验证。

主解:分组背包

对每个仓库,把货物价值降序排序。若该仓库选前 kk 件,费用为

cost=bj+kcj,cost = b_j + k \cdot c_j,

净现金为前 kk 件价值之和减去这个费用。于是一个仓库产生 k=1仓库货物数k = 1 \dots \text{仓库货物数} 个“费用—收益”互斥选项。

dp[c] 表示已经处理完若干仓库、恰好花费 cc 时能获得的最大净现金。处理每个仓库时,先复制一份旧 dp 表示“不选该仓库”,再枚举选择前 kk 件的每个选项,从旧 dp 转移。同一仓库的不同选项互斥,所以必须从旧状态转移而不是滚动覆盖。

费用上界

由于 bj,cj20b_j, c_j \le 20,全部货物都卖出时的总费用不超过

20n+20m40000,20n + 20m \le 40000,

可以按费用开数组,最后扫描满足 dp[c] >= v 的最小 cc

代码

cpp
/**
 * 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-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1005;
const int MAXC = 40005;

int n, m, target;
int basic_cost[MAXN];   // 仓库 i 的基本运费
int piece_cost[MAXN];   // 仓库 i 每件货物的计件运费
vector<int> goods[MAXN]; // goods[i] 存放仓库 i 中所有货物的价值
int dp[MAXC];    // dp[c] 表示恰好花费 c 时能获得的最大净现金
int next_dp[MAXC]; // 处理当前仓库时的滚动数组

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

    cin >> n >> m >> target;
    for (int i = 0; i < n; i++) cin >> basic_cost[i] >> piece_cost[i];
    for (int i = 0; i < m; i++) {
        int value, warehouse;
        cin >> value >> warehouse;
        goods[warehouse].push_back(value);
    }

    // 同一仓库选 k 件时应取价值最高的 k 件,先降序排序;同时算出最大可能花费
    int maximum_cost = 0;
    for (int i = 0; i < n; i++) {
        if (goods[i].empty()) continue;
        sort(goods[i].begin(), goods[i].end(), greater<int>());
        maximum_cost += basic_cost[i] + piece_cost[i] * (int)goods[i].size();
    }

    // 分组背包:把每个仓库看作一组,组内选项是“选前 k 件”的所有 k
    const int NEG_INF = -1000000000;
    for (int c = 0; c <= maximum_cost; c++) dp[c] = NEG_INF;
    dp[0] = 0;

    for (int warehouse = 0; warehouse < n; warehouse++) {
        if (goods[warehouse].empty()) continue;
        // next_dp 先复制 dp,表示“不选这个仓库”的情况
        for (int c = 0; c <= maximum_cost; c++) next_dp[c] = dp[c];

        int profit = -basic_cost[warehouse];
        for (int count = 1; count <= (int)goods[warehouse].size(); count++) {
            profit += goods[warehouse][count - 1] - piece_cost[warehouse]; // 选前 count 件的净现金
            int cost = basic_cost[warehouse] + count * piece_cost[warehouse];
            // 从旧 dp 转移,保证同一仓库的不同选项互斥
            for (int old_cost = 0; old_cost + cost <= maximum_cost; old_cost++) {
                if (dp[old_cost] == NEG_INF) continue;
                if (next_dp[old_cost + cost] < dp[old_cost] + profit) {
                    next_dp[old_cost + cost] = dp[old_cost] + profit;
                }
            }
        }
        for (int c = 0; c <= maximum_cost; c++) dp[c] = next_dp[c];
    }

    // 找满足净现金 >= target 的最小花费
    int answer = maximum_cost;
    for (int c = 0; c <= maximum_cost; c++) {
        if (dp[c] >= target && c < answer) answer = c;
    }
    cout << answer << '\n';

    return 0;
}

复杂度

设货物总数 mm、最大费用 C40000C \le 40000

  • 时间:每个仓库的选项总数为 O(n+m)O(n + m),分组转移 O(C(n+m))O(C(n + m)),排序 O(mlogm)O(m \log m)
  • 空间:DP 数组 O(C)O(C),货物存储 O(m)O(m)

总结

把“同仓库选哪些货物”先压缩为“选前 kk 件”,再用费用上界做分组背包,就把指数级子集选择变成了可控的费用 DP。分组背包的要点是同一组内选项互斥,转移必须基于处理该组之前的旧状态。