完全背包
无限件·一维正推
本课摘要
完全背包课程回答“物品可无限取用时,正序更新如何复用本轮状态”。内容以无限件·一维正推为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断完全背包的适用条件与状态边界
- 围绕“无限件·一维正推”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
同一种物品,取之不尽
完全背包与 01 背包只差一个字:01 里每件要么取要么留,完全里每种物品有无限件,同一种想拿几件就拿几件。 目标不变,在不超过容量的前提下,让装入的总价值最大。
状态定义也不用改: 仍表示容量不超过 时的最大价值。变的只有一件事, 「考虑第 种物品」这个动作,现在可以对同一种反复施加,而不是只做一次决断。
与 01 只差一个方向:正推即「允许重复」
转移方程写出来,和 01 背包的一维式子一模一样:
差别只在循环方向:01 背包逆推( 从 到 ,保证每件至多取一次), 完全背包正推( 从 到 )。就这一处方向之差,决定了「每种一件」还是「每种无限件」。
本质 · 为什么正推就对了
正推时算 用到的 ,可能已经包含了第 种,于是这一种被自然地再取一次。 这正是 01 背包「不能正推」那一节里的同一个机制:在 01 里它是要极力避开的 bug,在完全背包里它恰恰是我们想要的特性。同一段转移,方向决定物种。
为什么还是 O(nm):从枚举件数到一次转移
「无限件」听起来更复杂,最朴素的想法是枚举第 种取几件:取 件各算一遍再取最大,
这比 01 多了一层「枚举件数」,复杂度升到 。但盯住 看:它本身已经是「前 种、容量 」把所有件数都枚举过的最优,已经包含了「再多取一件第 种」的全部可能。于是那一整层枚举可以折叠成一步:
降到一维后, 与 共用同一个数组,「本行的 」正是正推时那个已被本种更新过的值,上一节循环方向的由来,到这里就完全说通了。
跟着算一遍:看它把一件反复拿
拿一件物品 、容量 6,把正推 走一遍:
看它累积起来
改物品与容量,观察 如何沿正方向累积,同一件物品在一条链上被反复加进来。这与 01 背包的 「顺推 bug」是同一个机制,只是这里它是特性而非缺陷。
自主设计数值
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
01 还是完全?并排看差别
同一组物品、同一容量,左边按 01(每种至多 1 件)、右边按完全(每种无限件)各算一遍,改改 和容量, 看完全背包如何靠反复取用同一种,拿到不低于 01 的价值。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
例题
long long。若仍写成 P1048 常见的 int f[10005],大数据会越界或溢出。#include <iostream>
#include <algorithm>
using namespace std;
int t[10005], v[10005];
long long f[10000005]; // 1 <= T <= 10^7,答案也可能超过 int
int main()
{
int T, M;
cin >> T >> M;
for (int i = 1; i <= M; i++)
cin >> t[i] >> v[i];
for (int i = 1; i <= M; i++)
for (int j = t[i]; j <= T; j++) // ★正推:允许同一物品被重复取
f[j] = max(f[j], f[j - t[i]] + v[i]);
cout << f[T] << endl;
return 0;
}#include <iostream>
#include <algorithm>
using namespace std;
int p[105][105]; // p[d][j]:第 d 天第 j 种纪念品价格
int f[100005];
int main()
{
int T, n, m;
cin >> T >> n >> m;
for (int d = 1; d <= T; d++)
for (int j = 1; j <= n; j++)
cin >> p[d][j];
for (int d = 1; d < T; d++) // 枚举每一天,用当天买、次日卖
{
for (int j = 0; j <= m; j++) f[j] = 0; // 每天现金独立,重置
for (int j = 1; j <= n; j++) // 每种纪念品可买多份 → 完全背包
for (int c = p[d][j]; c <= m; c++) // ★正推
f[c] = max(f[c], f[c - p[d][j]] + p[d + 1][j] - p[d][j]);
m += f[m]; // 当天最优收益并入本金
}
cout << m << endl;
return 0;
}#include <iostream>
#include <algorithm>
using namespace std;
int a[105];
bool f[25005]; // f[j]:用已保留的面值能否凑出金额 j
int main()
{
int T;
cin >> T;
while (T--)
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1); // 从小到大处理
int m = a[n]; // 最大面值即可达范围上界
for (int j = 0; j <= m; j++) f[j] = false;
f[0] = true;
int cnt = 0;
for (int i = 1; i <= n; i++)
if (!f[a[i]]) // 这个面值凑不出来 → 必须保留
{
cnt++;
for (int j = a[i]; j <= m; j++) // 完全背包正推标记可达
f[j] = f[j] || f[j - a[i]];
}
cout << cnt << endl;
}
return 0;
}
