体积价值均不超过 100 的完全背包:同体积去重后取密度最大物品贪心装满 ⌊m/a1⌋−100 件,剩余不超过 10099 的容量交给一维完全背包修正零头,绕开 10^16 的容量上限。
OJ: roj
题目 ID: 10023
难度:提高+/省选-
标签:动态规划完全背包背包贪心python
创建: 2026-10-02 18:14
更新: 2026-10-07 11:01
形式化题目
给定正整数容量
题面的约束极不对称:物品种数
样例 1:
样例 2:
正解
思路
朴素做法:一维完全背包。
正序更新等价于每件物品可以重复取。时间
第一刀:同体积去重。 体积相同的两件物品,价值低的那件永远不会被用到——无限件背包里随时可以拿同体积更高价值的替换它。体积只有
第二刀:密度贪心。 定义密度
即按体积折算,把其它物品换成
第三刀:贪心段 + 余量 DP 段。 把容量切成两段:
- 贪心段:先装
件最优物品,价值 ; - DP 段:剩余容量
,当 时 ;对这段容量用朴素的一维完全背包算出 (物品仍是去重后的那至多 100 个)。
答案
flowchart LR
A["容量 m<br/>(最大 10^16)"] --> B["密度最大的物品 (a1,b1)"]
B --> C["贪心段<br/>装 cnt = max(0, ⌊m/a1⌋ − 100) 件<br/>价值 cnt × b1"]
C --> D["余量 DP 段<br/>rest = (m mod a1) + 100·a1<br/>≤ 10099"]
D --> E["一维完全背包<br/>至多 100 个物品 × 10099 容量"]
C --> F["答案 = cnt × b1 + f[rest]"]
E --> F
为什么恰好留 100 份。 100 是题面给的体积上限:任何其它物品的体积都不超过 100,贪心可能吃亏的规模(零头
正确性(两面夹逼)。
先证一个引理——贪心补满引理:任何一个装法都可以拆成"若干其它物品构成的装法
- 候选值
最优值(可行性): 件 加上 DP 段的方案,总体积 ,是真实可行的方案,所以 不会超过真正的最优。 - 最优值
候选值:把 代入引理, ,提出 后按 分两支: : 正是对容量 使用同一引理的式子,即 ,而 ,此支 。 :此时 ,此支价值为 ,其中 。构造一个 DP 段方案与之比较:从 里删掉总体积 的一部分物品(剩下的体积 ,正好放进 DP 段),空隙用 补齐。删除部分按密度折算的价值 不超过同体积贪心物品的价值,且 的预留保证删除体积能对齐到 的整数倍附近(物品体积 ,区间长度 ,总能取到合适的 ),因此构造方案的价值 ;而 取 DP 段所有方案的最大值,此支同样 。
两支都不超过
样例 1 走一遍。
| 处理阶段 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 只放 |
0 | 0 | 0 | 2 | 2 | 2 | 4 | 4 | 4 | 6 | 6 | 6 | 8 | 8 | 8 | 10 |
| 再放 |
0 | 0 | 0 | 2 | 2 | 3 | 4 | 4 | 5 | 6 | 6 | 7 | 8 | 8 | 9 | 10 |
加入
代码
正式解 main.py:逐行流式读入并按体积去重(best),取密度最大的物品,切出贪心段与 DP 段,最后相加。
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-02 18:14
# update_at: 2026-10-02 18:14
import sys
RESERVE = 100 # 贪心段预留的体积份数:正好是体积上限,误差留给 DP 段修正
def knapsack(items: list[tuple[int, int]], cap: int) -> list[int]:
"""完全背包:f[c] = 体积不超过 c 时的最大价值(正序滚动即每件无限取)。"""
f = [0] * (cap + 1)
for a, b in items:
for j in range(a, cap + 1):
f[j] = max(f[j], f[j - a] + b)
return f
def solve() -> None:
inp = sys.stdin.buffer
n, m = map(int, inp.readline().split())
# 同体积只留价值最大的一件:其余同体积物品被它严格支配
best: list[int] = [0] * 101
for _ in range(n):
a, b = map(int, inp.readline().split())
if b > best[a]:
best[a] = b
items = [(a, best[a]) for a in range(1, 101) if best[a]]
# 密度(价值/体积)最大的物品:任何其它物品换到同体积都换不出更多价值
a1, b1 = max(items, key=lambda p: p[1] / p[0])
greedy = max(0, m // a1 - RESERVE) # 贪心段:先塞满这么多件最优物品
rest = m - greedy * a1 # DP 段:≤ 100*a1 + 99 ≤ 10099,只算这一小段
print(greedy * b1 + knapsack(items, rest)[rest])
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-04 23:07
* update_at: 2026-10-04 23:07
*/
// main.cpp:完全背包。
// m 高达 10^16,不能对整个容量做 DP:
// 1. 同体积去重:体积 <= 100,每个体积只留价值最大的物品,至多 100 个;
// 2. 密度贪心:取价值/体积密度最大的物品 (a1,b1),
// 先装 cnt = max(0, m/a1 - 100) 件(大数部分用一次乘法承接);
// 3. 余量 rest = m - cnt*a1 <= 99 + 100*a1 = 10099,
// 对这段小容量做一维完全背包修正零头。
// 答案 = cnt * b1 + f[rest],全程与 m 无关。
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int MAXV = 100; // 体积上限 100
const int MAXREST = 10099; // DP 段容量上界:99 + 100*100
const ll RESERVE = 100; // 给零头预留的份数(取自体积上限)
ll n, m;
ll best[MAXV + 1]; // best[v] = 体积为 v 的物品中的最大价值(0 表示该体积没出现过)
ll f[MAXREST + 1]; // f[j] = 容量 j 内能拿到的最大价值(一维完全背包)
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
// 流式读入 n 件物品,同体积只留价值最大的,把 n <= 10^6 压到至多 100 个
for (ll i = 1; i <= n; ++i) {
ll a, b;
cin >> a >> b;
if (b > best[a])
best[a] = b;
}
// 找密度(b/a)最大的物品 (a1, b1),它是"万能填充物"
ll a1 = 0, b1 = 0;
for (int v = 1; v <= MAXV; ++v) {
if (best[v] == 0) continue;
// 交叉相乘比较 b/v > b1/a1,避免浮点误差
if (a1 == 0 || best[v] * a1 > b1 * v) {
a1 = v;
b1 = best[v];
}
}
// 贪心段:装 cnt 件最优物品,留出 100 份容量给 DP 修正零头
ll cnt = m / a1 - RESERVE;
if (cnt < 0) cnt = 0;
ll rest = m - cnt * a1; // 余量 <= 99 + 100*a1 <= 10099
// DP 段:对容量 rest 做一维完全背包,正序扫描表示每种物品可重复取
ll limit = rest;
for (ll j = 0; j <= limit; ++j) f[j] = 0;
for (int v = 1; v <= MAXV; ++v) {
if (best[v] == 0) continue;
for (ll j = v; j <= limit; ++j) {
if (f[j - v] + best[v] > f[j])
f[j] = f[j - v] + best[v];
}
}
cout << cnt * b1 + f[rest] << endl;
return 0;
}knapsack 就是思路里 DP 段的一维完全背包;RESERVE = 100 即预留份数;greedy、rest 分别是贪心段件数与余量容量;max(0, ...) 处理
复杂度
- 时间:读入
( ,逐行流式,不整体载入);DP 段 ,其中 个去重物品、 ,不超过 次转移。总计约 次基本操作,与 无关,与正解( std.cpp的贪心 + 小容量背包)同阶。 - 空间:
加输入流式常数,实测峰值约 15MB,远低于 128MB 限制。 - 验证:官方下发的 10 组真实数据(
最大约 )本地实测输出全部一致;Python 侧唯一风险是 OJ 的 1s 限时,按任务约定允许 TLE。
总结
- 瓶颈永远在最贵的那个维度上:
,所以 DP 只能算"与 无关"的一小段。 - 同体积去重(支配关系)把
个输入压到至多 100 个物品;密度最大的物品是"万能填充物",大数部分用一次乘法承接。 - 贪心装
件、余量 留给完全背包修正零头;100 正是体积上限,预留量直接取自题面约束。 - 正确性靠贪心补满引理拆成两支夹逼:可行侧显然,最优侧两支都放不进
之上,等号成立。 - 复杂度
、空间 ,全程与 无关。