背包综合变形
方案数·撤销·具体方案
本课摘要
背包综合变形课程回答“背包模型如何扩展到计数、撤销和方案恢复”。内容以方案数·撤销·具体方案为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断背包综合变形的适用条件与状态边界
- 围绕“方案数·撤销·具体方案”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
从「求最值」到「换一个聚合算子」
到这里,背包的容量骨架已经很熟:枚举物品、逐容量转移、一维 由 推来。 前面几类都在问同一件事,价值最大是多少,所以转移里坐着一个 。可现实里的问题未必都求最优: 「恰好花光 元有多少种点法?」「这堆砝码能不能称出重量 ?」
关键洞察是:背包的骨架和「求什么」是解耦的。把转移中的 换成加法 , 的含义就从「最大价值」变成「凑出 的方案数」;换成逻辑或 ,就变成「 能否被凑出」的布尔判定。 物品怎么取、循环怎么转,一个字都不用改。这一节就把最常考的一支,方案数背包,讲透,再看它的一个漂亮延伸:撤销。
方案数:把 max 换成加法,f[0] 换成 1
设 表示恰好装满容量 的方案数。对第 件物品(重量 ), 凑出 的方案分两类:不含它,数目已记在旧的 里;含它,先把它占的 抠掉, 剩下的 由前面的物品去凑,方案数正是 。两类不重不漏,加起来就是新的 :
和 01 背包一样倒序,每件至多计入一次,倒序让 停在「这件还没参与」的旧值上。真正的分水岭在初值:
为什么 ?因为「凑出容量 0」有且只有一种办法,什么都不装(空方案)。这个 1 是所有计数的种子: 它顺着 一路传播,每落到一个能被凑出的容量,就点亮一种新组合。若把它写成 0,整张表会永远是 0,一种方案也数不出来。
本质 · 算子决定问题,骨架不动
背包框架回答的是「用这些物品凑容量」这件事本身;把结果如何聚合,是另一个正交的维度。(最优)、(计数)、(可行)只是同一骨架上换插头。 计数型的两处硬改动就记死:、。
还有一个常混的点:「恰好装满」还是「不超过」?看你把种子撒在哪、答案读哪格。 要「恰好装满 」,就只让 (唯一合法的空起点),答案读 ; 若问「总重不超过 的方案数」,则把 全设成 1(任何容量都允许「空着」),或最后对 求和。本页例题走的都是「恰好」这一支。
跟着算一遍:两条组合各贡献 1
用三件物品 、目标容量 走一遍。手上先想清答案:恰好凑出 5 的子集只有 和 两个,所以 该等于 2。看表怎么把这 2 数出来:
看方案数一格一格叠出来
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
深化 · 撤销:正难则反,把某件「退」出去
方案数背包有一个极漂亮的延伸。设想这样的问题(洛谷 P4141「消失之物」): 个物品, 对每一个物品 ,都要回答「假如第 件消失了,凑出体积 的方案数是多少」。 最笨的办法是抠掉一件、重算一遍整张表, 件就是 遍,,太慢。
正难则反:与其一件件「不放进去」,不如先把全部物品都放进去算出全集方案数 , 再针对要消失的那件做一次逆操作,把它对 的贡献「退」掉。当初加它是 , 那么退它就是它的逆:
方向是这里唯一的陷阱。回想计数为什么倒序:为了让 保持「本件还没加入」的干净旧值。撤销要的恰恰相反, 算 时,我需要 已经是「本件退干净」的值,这样减出来的 才不含第 件。 而 ,所以必须让小下标先被退,也就是 从 正序涨到 。 把方向记反,退出来的就是一堆错数。
常见陷阱 · 撤销的方向与加时相反
加一件物品用倒序(),撤一件物品用正序(),这不是可选项,是逆操作的内在要求: 撤销时 依赖已经退干净的 ,故小下标必须先处理。此外别在原数组上直接减(会污染下一件的撤销), 每次从全集 拷一份 再退;带模数时减法记得 再取模,避免出现负数。
看它把一件「退」出去
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
例题
#include <iostream>
using namespace std;
int a[105];
long long f[10005]; // f[j]:恰好花 j 元的方案数(方案数常爆 int,用 long long)
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
f[0] = 1; // ★地基:花 0 元有 1 种方案(什么都不点)
for (int i = 1; i <= n; i++)
for (int j = m; j >= a[i]; j--) // 倒序:每道菜至多点一次(01)
f[j] += f[j - a[i]]; // ★把 max 换成累加,就从「求最优」变「数方案」
cout << f[m] << endl;
return 0;
}#include <iostream>
using namespace std;
const int MOD = 10;
int w[2005];
int f[2005], g[2005]; // f:含全部物品的方案数;g:撤销某件后的临时方案数
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> w[i];
f[0] = 1; // 全集方案数:标准计数背包
for (int i = 1; i <= n; i++)
for (int j = m; j >= w[i]; j--) // 加它:倒序
f[j] = (f[j] + f[j - w[i]]) % MOD;
for (int i = 1; i <= n; i++) // 逐个「消失」的物品 i
{
for (int j = 0; j <= m; j++)
g[j] = f[j]; // 从全集出发
for (int j = w[i]; j <= m; j++) // ★退它:正序,方向与加时相反
g[j] = (g[j] - g[j - w[i]] + MOD) % MOD; // 逆操作:把第 i 件的贡献减掉
for (int j = 1; j <= m; j++) // 缺第 i 件时,体积 j 的方案数
cout << g[j];
cout << endl;
}
return 0;
}
