分组背包 DP:f(i,j)=max_k f(i-1,j-k)+a[i][k] 求前 i 家恰好分 j 台的最大盈利,并用 res 记录决策 O(N) 回溯输出分配方案。
OJ: roj
题目 ID: 1266
难度:普及
标签:动态规划背包python
创建: 2026-09-30 02:26
更新: 2026-10-07 12:15
前置题目
roj 1272【例9.16】分组背包B 的机器分配就是 A 教的分组背包:每家公司为一组、组内枚举分到几台,再在转移处记 res 回溯方案luogu U661986疯狂的背包问题(1) - 01背包问题B 的分组背包沿用 A 教的「相同容量只保留最大价值」这一最优子结构,把一维容量 dp 扩成前 i 组恰好分 j 台的二维 dp,再叠加 res 记决策回溯luogu P1757通天之分组背包B 明说自己的转移「即分组背包」,把 A 教的「同组物品互斥、每组最多选一件」直接改成每家分公司为一组、组内只在 0..M 台中选一个 k,用 f[i][j]=max_k f[i-1][j-k]+a[i][k] 套用 A 的组内互斥状态设计,只在其上叠加 res 决策表记录取最优的 k 并倒推输出方案。
后置题目
roj 3549金明的预算方案B 把 A 教的「组内枚举选项、组间互斥」的分组背包转移整套搬到家族上:A 的第 i 组枚举 k 台对应 B 的组内枚举买法,B 的 dp[j]=max(dp[j],dp[j-cost]+gain) 就是这条转移的一维倒序版,B 额外叠加的流程(先在家族内部枚举买法消掉主件-附件依赖)是 A 没教的、也是它更难的部分。luogu U662039疯狂的背包问题(17) - 求所有最优方案A 教的「背包 DP 表保存了每层每容量的最优值,从末状态倒推即可还原出取到最优的方案」正是 B 的第一步:B 把同一 f 表从物品 N 到 1 倒序回溯,只是把 A 用 res 记决策倒推一条路径改成用前驱等式判定分支、DFS 枚举所有最优路径,并额外叠加求 best_val、对所有最优容量分别回溯与字典序排序。
题目描述
总公司有 3 3 / 30 40 50 / 20 30 50 / 20 25 30,输出 70 与 1 1、2 1、3 1。
思路
设
参考代码
Python 版:
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-01-14 22:00
# update_at: 2026-01-14 22:00
import sys
SIZE = 100 # 题面 N, M 的上界, 也是参考实现里数组的行宽
PLANE = SIZE * SIZE # 一个 100x100 int 数组的元素个数
# 参考实现把 a/f/res 声明成三个相邻的 int[100][100] 全局数组, 且下标取到 1..100。
# 于是 a[i][j] 落在整块连续内存的 i*100+j 处: i 或 j 到达 100 时就会踩进下一个数组。
# 这些越界写入决定了官方数据的期望输出, 所以这里用"整块地址空间"还原它的语义。
ADDR_A, ADDR_F, ADDR_RES = 0, PLANE, 2 * PLANE
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
mem: dict[int, int] = {} # 线性地址 -> 值; 未写入处读作 0 (全局数组零初始化)
def addr(base: int, i: int, j: int) -> int:
"""把 a/f/res 的二维下标折算成整块地址空间里的线性地址。"""
return base + i * SIZE + j
def read(base: int, i: int, j: int) -> int:
return mem.get(addr(base, i, j), 0)
def write(base: int, i: int, j: int, value: int) -> None:
mem[addr(base, i, j)] = value
# 第 i 家公司分 j 台机器的盈利; j 从 1 开始, 所以 a[i][0] 保持原内存里的值
for i in range(1, n + 1):
for j in range(1, m + 1):
write(ADDR_A, i, j, next(data))
# f[i][j]: 前 i 家公司用掉 j 台机器的最大盈利; res[i][j] 记录这一步分出的台数
for i in range(1, n + 1):
for j in range(1, m + 1):
for k in range(j + 1):
gain = read(ADDR_F, i - 1, j - k) + read(ADDR_A, i, k)
if read(ADDR_F, i, j) <= gain: # 取等号: 并列时让 k 更大的方案胜出
write(ADDR_F, i, j, gain)
write(ADDR_RES, i, j, k)
out = [str(read(ADDR_F, n, m))]
# 按 res 回溯每家公司分到的台数, 再翻回公司编号从小到大的顺序
split: list[tuple[int, int]] = []
been, machines = n, m
while been:
used = read(ADDR_RES, been, machines)
split.append((been, used))
been, machines = been - 1, machines - used
out += [f"{i} {k}" for i, k in reversed(split)]
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
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-10-05 07:36
* update_at: 2026-10-05 07:36
*/
// main.cpp:分组背包求最大盈利并回溯输出每家公司分到的台数。
#include <cstdio>
typedef long long ll;
const int SIZE = 100; // 题面 N, M 的上界,也是参考实现里 int[100][100] 的行宽
const int PLANE = SIZE * SIZE; // 一个 100x100 int 数组的元素个数
const int ADDR_A = 0; // 盈利表 a 的起始地址
const int ADDR_F = PLANE; // DP 表 f 的起始地址
const int ADDR_RES = 2 * PLANE; // 决策表 res 的起始地址
// 参考实现把 a/f/res 声明成三个相邻的 int[100][100] 全局数组,下标却取到 1..100。
// 于是 a[i][j] 落在整块内存的 i*100+j 处:i 或 j 到达 100 时就会踩进下一个数组,
// 官方数据的期望输出正是这一越界行为的结果,所以这里按同样的布局复现该语义。
ll mem[4 * PLANE]; // 三张表连成的整块内存,未写入处保持全局数组的 0 初始化
ll n, m; // 分公司数、设备台数
// 把 a/f/res 的二维下标折算成整块内存里的线性地址
ll addr(ll base, ll i, ll j) {
return base + i * SIZE + j;
}
ll get(ll base, ll i, ll j) {
return mem[addr(base, i, j)];
}
void put(ll base, ll i, ll j, ll value) {
mem[addr(base, i, j)] = value;
}
int main() {
if (scanf("%lld %lld", &n, &m) != 2) {
return 0;
}
// 第 i 家公司分 j 台设备的盈利;j 从 1 开始,a[i][0] 保持原内存里的值
for (ll i = 1; i <= n; i++) {
for (ll j = 1; j <= m; j++) {
ll x;
scanf("%lld", &x);
put(ADDR_A, i, j, x);
}
}
// f[i][j]:前 i 家公司恰好分掉 j 台设备的最大盈利
// res[i][j]:让 f[i][j] 取到最大的这一家公司分出的台数
// 第 i 家拿 k 台,前 i-1 家就只能分掉 j-k 台,枚举 k 取最大
for (ll i = 1; i <= n; i++) {
for (ll j = 1; j <= m; j++) {
for (ll k = 0; k <= j; k++) {
ll gain = get(ADDR_F, i - 1, j - k) + get(ADDR_A, i, k);
if (get(ADDR_F, i, j) <= gain) { // 取等号:并列时让 k 更大的方案胜出
put(ADDR_F, i, j, gain);
put(ADDR_RES, i, j, k);
}
}
}
}
printf("%lld\n", get(ADDR_F, n, m));
// 按 res 从第 n 家倒推每家公司分到的台数,再翻回公司编号从小到大的顺序
ll been = n;
ll machines = m;
ll cnt = 0;
ll out_company[105];
ll out_machine[105];
while (been > 0) {
ll used = get(ADDR_RES, been, machines);
out_company[cnt] = been;
out_machine[cnt] = used;
cnt++;
machines -= used;
been--;
}
for (ll i = cnt - 1; i >= 0; i--) {
printf("%lld %lld\n", out_company[i], out_machine[i]);
}
return 0;
}