背包DP教程:从DFS暴力到DP的逐步推导
ZhangChuan1350 · · 算法·理论
前言
:::epigraph[放在前面] 本文源自一位初二年级 OIer 的真实学习笔记与教学反思,愿与你分享从困惑到通透的完整路径。 :::
背包问题是动态规划(DP)最经典的基石。但回想我自己的学习经历,最大的障碍不是问题本身,而是大多数教程都从完美的状态方程
在本文中,我将介绍背包模型的 DFS、记忆化搜索、二维数组、滚动数组和其他优化,由浅入深。其中,本文最具特色的是拥有 DFS 和记忆化搜索的解释,在洛谷中背包 DP 的极少文章能拥有这两点。
以前,当我还是初学者的时候,就是直接去看一维 DP,导致背包理解不透彻,竟然以为是按照“背包容量”为状态变量。这篇文章的结构,就是复制了我本人的学习经验,那时从困惑到顿悟的学习路线,而不是从教科书上抄来的最优解。
我为什么发表这篇文章? 就是因为我的很多同学都直接奔着最优解而去,而恰恰忽略了朴素但强大的 DFS,导致理解出问题。所以,我决定写下这篇笔记,从最根源的搜索思维讲起。
01 背包
模型: 给定
暴力搜索 DFS 方法
很明显,我们面对第
当我们理解这个思想的时候,就可以写出代码了。
选是一种情况,不选也是一种情况,分别枚举即可。
-
j=4$:$\max(f_4,f_2+3)=\max(0,0+3)=3 -
j=3$:$\max(f_3,f_1+3)=\max(0,0+3)=3 -
j=2$:$\max(f_2,f_0+3)=\max(0,0+3)=3 -
结果:每个物品只选了一次! ::: :::error[误用正序的后果]
-
j=2$:$\max(0,f_0+3)=3 -
j=3$:$\max(0,f_1+3)=3 -
::: **注意**:我们不是只以背包容量作为阶段变量!只是降维!
推荐练习题目:
- 洛谷 P1048 采药 - 01背包模版
- 洛谷 P2871 魅力手链 - 01背包模版
- 洛谷 P1049 装箱问题 - 01背包变形
- 洛谷 P1060 开心的金明 - 01背包变形
01背包总结
实现方法 时间复杂度 空间复杂度 缓存友好度 代码复杂度 朴素 DFS O(2^n) O(n) 差 极简 记忆化搜索 O(nm) O(nm) 差 较复杂 二维数组 O(nm) O(nm) 一般 简单 滚动数组 O(nm) O(m) 较高 中等 降维优化 O(nm) O(m) 高 简单
提示:
- 记忆化搜索由于递归深度,空间复杂度在
O(nm) 的基础上加O(n) 。
因空间复杂度的标准写法,这里不加上O(n) ,只要知道就行,后面不再赘述。 - 记忆化搜索由于递归原因,时间复杂度的常数因子比二维数组要大。
适用场景:
- 理解阶段:DFS 和记忆化搜索必须掌握。
- 学习阶段:推荐二维数组,易于理解。
- 竞赛小数据:滚动数组,平衡性能和可读性。
- 竞赛大数据:一维降维,极致性能。
:::warning[易错点]
- 记忆化搜索或朴素 DFS 由于
n 过大,导致栈溢出,引发 RE。 - 二维数组和滚动数组第
i 行没有继承第i-1 行导致错误。 - 滚动数组误用位运算导致结果错误。
- 降维优化正序遍历
j 导致错误(应倒序遍历)。 - 降维优化本质上是在一行内操作,仅省略了阶段变量
i ,因此需要完全理解二维数组的状态转移再进行学习降维优化。 :::完全背包
:::success[如何理解完全背包] 我第一次学完全背包时,和你想的一模一样:用一个循环
for (int k=0;k*w[i]<=j;k++)来枚举选几件。这是最直观、最符合暴力搜索思维的方法。
但是后来我发现:这种“枚举k ”的思路写出来的代码是三重循环,效率太低。
真正的突破是当我发现:如果允许重复选,那么状态转移方程中,选第
这个发现让我震惊:原来 01 背包和完全背包的本质区别,就是这一个下标的差别!这直接导致了代码中
所以,在本文中,我会先带你走我走过的路——从枚举
暴力搜索 DFS 方法
首先,完全背包与01背包不同的是:01背包每件物品只能选择
如果是01背包,我们直接枚举它选或不选即可,但完全背包呢?
我们可以用一个状态变量
根据实际情况,
整体代码如下:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int ans=0; //存储最终结果
int w[105],v[105];
void dfs(int i,int j,int cnt){
//i代表第i个物品,j代表当前背包剩余容量,cnt代表当前已选物品的总价值
if (i>n){ //选完所有物品
ans=max(ans,cnt); //更新最大价值
return;
}
//选择k个第i件物品,进行枚举(包含不选,也就是k=0的情况下)
for (int k=0;k*w[i]<=j;++k){
dfs(i+1,j-k*w[i],cnt+k*v[i]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
scanf("%d%d",&w[i],&v[i]);
}
//从第1个物品选择,当前剩余容量为m,当前已选物品总价值为0(实际上还没选)
dfs(1,m,0);
printf("%d",ans);
return 0;
}
由于完全背包的第
观察完全背包的选择路线,我们可以发现,有很多重复计算的节点,因此,我们可以像01背包一样使用记忆化搜索,用一个
类似地,我们用直接枚举的方法,代码就是这样的:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int ans=0; //存储最终结果
int w[105],v[105];
void dfs(int i,int j,int cnt){
//i代表第i个物品,j代表当前背包剩余容量,cnt代表当前已选物品的总价值
if (i>n){ //选完所有物品
ans=max(ans,cnt); //更新最大价值
return;
}
dfs(i+1,j,cnt); //不选第i件物品
if (j>=w[i]){
dfs(i,j-w[i],cnt+v[i]); // 最特别地!!!
// 这里的i不加1,是因为我们还可以重复选择这件物品
// cnt+v[i] 表示我们选择了这件物品,增加了它的价值
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
scanf("%d%d",&w[i],&v[i]);
}
//从第1个物品选择,当前剩余容量为m,当前已选物品总价值为0(实际上还没选)
dfs(1,m,0);
printf("%d",ans);
return 0;
}
记忆化搜索 方法
首先,由于背包受到
但是,由于这样的代码实在过于复杂,而且空间复杂度到了不可承受的地步,只要
因此,我们可以换一种思路:不使用
整体代码如下:
#include <bits/stdc++.h>
using namespace std;
int w[105],v[105],f[1005][10005];
int m,n;
int dfs_memo(int i, int j){
//i表示第i件物品,j表示当前背包剩余容量为j
if (i>n) return 0; //选完所有物品
if (f[i][j] != -1) return f[i][j];
int res = dfs_memo(i+1, j); //不选第i个物品
for (int k=1;k*w[i]<=j;++k){
//虽然不用k标记,但实际操作中需要使用k
int choose = dfs_memo(i+1, j-k*w[i])+k*v[i];
res = max(res, choose);
}
return f[i][j] = res;
}
int main()
{
memset(f,-1,sizeof f);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
scanf("%d%d",&w[i],&v[i]);
}
printf("%d",dfs_memo(1,m));
return 0;
}
同理,我们也可以用直接枚举的方法来书写代码。
整体代码如下:
#include <bits/stdc++.h>
using namespace std;
int w[105],v[105],f[1005][10005];
int m,n;
int dfs_memo(int i, int j){
//i表示第i件物品,j表示当前背包剩余容量为j
if (i>n) return 0; //选完所有物品
if (f[i][j] != -1) return f[i][j];
int res = dfs_memo(i+1, j); //不选第i个物品
if (j>=w[i]){
res = max(res, dfs_memo(i,j-w[i]) + v[i]);
//最重要的是i不能+1,否则就会退化为01背包,失去重复选择的特性
//因为还是i就表示还是选择第i件物品
}
return f[i][j] = res;
}
int main()
{
memset(f,-1,sizeof f);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
scanf("%d%d",&w[i],&v[i]);
}
printf("%d",dfs_memo(1,m));
return 0;
}
同样地,由于
这个时候就要用递推式的 DP 出场了。
完全背包 DP 方法
阶段划分
问题受
参考01背包和朴素 DFS,我们可以列出如下基础完全背包状态转移方程:
其中,
由于放入的物品总体积不能超过
代码实现
关键代码如下:
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
f[i][j] = f[i-1][j]; //继承,当k=0,即不选第i件物品的情况
for (int k=1;k*w[i]<=j;++k){
f[i][j]=max(f[i][j], f[i-1][j-k*w[i]]+k*v[i]); //比较选多少种好
}
}
}
最终结果存储在 f[n][m] 中,输出即可。
当然,在前面提到,我们可以用不断放物品,直到放不下为止,也就是
这个公式正好对应了上面的 DFS 思路:dfs(i, j-w[i], cnt+v[i]) 中的
关键代码如下:
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
f[i][j] = f[i-1][j]; //继承,当k=0,即不选第i件物品的情况
if (j>=w[i]){
f[i][j]=max(f[i][j], f[i][j-w[i]]+v[i]);
}
}
}
这样,时间复杂度优化为
完全背包滚动数组优化
我们发现,完全背包的第
定义一个数组
关键代码如下:
for (int i=1;i<=n;++i){
int curr=i%2; //当前行
int prev=(i-1)%2; //上一行
for (int j=0;j<=m;++j){ //从0开始,因为j=0时只能不选
f[curr][j] = f[prev][j]; // 不选当前物品
if (j>=w[i]){
f[curr][j]=max(f[curr][j], f[curr][j-w[i]]+v[i]);
//这里依赖的是f[curr][j-w[i]],即当前行的值,体现了完全背包可重复选择的特性
}
}
}
该代码与01背包不同的就是当
同样地,由于我们是按照顺序处理的,所以答案同样存储在 f[n%2][m]。
完全背包降维优化
前面讲过,01背包需要倒序遍历才能使得只选一次,正序遍历会导致多次选择,完全背包正好符合正序遍历的要求,因此完全背包的降维优化是正序遍历的。
关键代码如下:
for (int i=1;i<=n;++i){
for (int j=w[i];j<=m;++j){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
最终结果存储在 f[m] 中,输出即可。
同样地,我们演示正序遍历的结果:
实例演示(物品:重量
结果:物品被重复选择,最大价值
- 洛谷 P1616 疯狂的采药 - 完全背包模版
- 洛谷 P1941 飞扬的小鸟- 完全背包模型
- 洛谷 P5020 货币系统 - 完全背包模型
- 洛谷 P1474 货币系统 - 完全背包应用
- 洛谷 P1025 数的划分 - 完全背包应用
- 洛谷 P1679 神奇的四次方数 - 完全背包变形
完全背包总结
实现方法 时间复杂度 空间复杂度 缓存友好度 代码复杂度 朴素 DFS O(m+1)^n O(n) 差 极简 记忆化搜索 O\left(nm\cdot\left\lfloor\dfrac m{w_i}\right\rfloor\right) O(nm) 差 较复杂 二维数组 1 O\left(nm\cdot\left\lfloor\dfrac m{w_i}\right\rfloor\right) O(nm) 一般 简单 二维数组 2 O(nm) O(nm) 一般 简单 滚动数组 O(nm) O(m) 较高 中等 降维优化 O(nm) O(m) 高 简单
提示:
- 记忆化搜索由于递归深度,空间复杂度在
O(nm) 的基础上加O(n) 。 - 记忆化搜索由于递归原因,时间复杂度的常数因子比二维数组要大。
- 如果二维数组采用第
2 个方案,则时间复杂度为O(nm) 。
与 01 背包的关键区别:
- 01 背包:逆序遍历,保证只选一次。
- 完全背包:正序遍历,允许重复选择。
- 状态转移:01背包依赖上一行,完全背包可依赖当前行。
- 核心差异在于状态转移时所依赖的上一状态是来自上一行(01背包)还是本行(完全背包),这直接决定了代码中
j 的遍历方向。
适用场景:
- 理解阶段:DFS 和记忆化搜索必须掌握。
- 学习阶段:推荐二维数组,易于理解。
- 竞赛小数据:滚动数组,平衡性能和可读性。
- 竞赛大数据:一维降维,极致性能。
:::warning[易错点]
- 记忆化搜索或朴素 DFS 由于
n 过大,导致栈溢出,引发 RE。 - 二维数组和滚动数组第
i 行没有继承第i-1 行导致错误。 - 二维数组第
2 种方法的状态转移错误(应从f_{i,j-w_i} 而不是f_{i-1,j-w_i} 转移到f_{i,j} )。 - 滚动数组误用位运算导致结果错误。
- 降维优化倒序遍历
j 导致错误(应正序遍历)。 - 降维优化本质上是在一行内完成操作,只是省略了阶段变量
i ,因此需要深刻理解二维数组的状态转移方程再进行学习降维优化。 :::多重背包
模型: 给定
n 种物品,其中第i 种物品的体积为w_i ,价值为v_i ,每种物品有c_i 件。有一体积为m 的背包,请选择一些物品放入背包,使得在物品总体积不超过m 的前提下,物品的价值总和最大。暴力搜索 DFS 方法
我们看到完全背包的朴素 DFS 中,
0\leqslant k\leqslant\left\lfloor\dfrac j{w_i}\right\rfloor ,这是由于完全背包可以无限选择所导致。
然而,多重背包还有物品数量的限制c_i ,这就要求k\ngtr c_i 。
综上,在多重背包里,k 的取值范围为0\leqslant k\leqslant\min\left(c_i,~\left\lfloor\dfrac j{w_i}\right\rfloor\right) 。这样保证每件物品选择数量不超过物品数量c_i 和背包容量\left\lfloor\dfrac j{w_i}\right\rfloor 。
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
int n,m,w[105],v[105],c[105];
int ans;
//这里的 w,v,c数组均与模型中描述的功能已知
void dfs(int i,int j,int cnt){
//i代表第i个物品
//j代表当前背包剩余容量为j
//cnt代表当前已选择的物品的总价值
if (i>n){
ans=max(ans, cnt);
return;
}
//k=0表示不选,其它的就是 k的取值范围
for (int k=0;k<=min(c[i],j/w[i]);++k){
dfs(i+1,j-k*w[i],cnt+k*v[i]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
scanf("%d%d%d",&w[i],&v[i],&c[i]);
}
dfs(1,m,0);
printf("%d",ans);
return 0;
}
由于多重背包的每件物品的决策有
记忆化搜索 方法
根据多重背包背包的最优子结构性质,我们可以使用一个
#include <bits/stdc++.h>
using namespace std;
int n,m,w[105],v[105],c[105];
int f[105][40005];
int dfs_memo(int i,int j){
//i表示第i件物品,j表示当前背包剩余容量为j
if (i>n) return 0; //选完所有物品
if (f[i][j]!=-1) return f[i][j];
int res=dfs_memo(i+1,j); //不选第i件物品
for (int k=1;k<=min(c[i],j/w[i]);++k){
//选择物品
int choose = dfs_memo(i+1, j-k*w[i])+k*v[i];
res=max(res, choose);
}
return f[i][j] = res;
}
int main()
{
memset(f,-1,sizeof f);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
scanf("%d%d%d",&v[i],&w[i],&c[i]);
}
printf("%d",dfs_memo(1,m));
return 0;
}
多重背包 DP 方法
阶段划分
问题受
参考完全背包,我们可以列出如下基础多重背包状态转移方程:
其中,
代码实现
关键代码如下:
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
f[i][j] = f[i-1][j]; //继承,不选当前物品
for (int k=1;k<=min(c[i],j/w[i]);++k){
f[i][j]=max(f[i][j], f[i-1][j-k*w[i]]+k*v[i]);
}
}
}
最终结果存储在 f[n][m] 中,输出即可。
:::success[基础解法过程]
实例演示(物品:重量
- 可选数量范围:
\min\left(2, \left\lfloor\dfrac 52\right\rfloor\right)=2 - 比较三种情况:
- 不选:
f_{i,5}=f_{i-1,5} - 选
1 个:f_{i-1,3}+3 - 选
2 个:f_{i-1,1}+6
- 不选:
- 取最大值作为最终结果
:::
多重背包滚动数组优化
与 01 背包、完全背包类似,多重背包也可以使用滚动数组优化空间。
关键代码:
for (int i=1;i<=n;++i){
int curr=i%2; //当前行(第i行)
int prev=(i-1)%2; //上一行(第i-1行)
for (int j=0;j<=m;++j){
f[curr][j] = f[prev][j]; // 不选当前物品
for (int k=1;k<=min(c[i],j/w[i]);++k){
f[curr][j]=max(f[curr][j], f[prev][j-k*w[i]]+k*v[i]);
}
//也可以让 k从 0到 min(c[i],j),这样就有不选的意思,但是我这样写可以更清晰
//两种写法性能差异不大
}
}
同样地,由于我们是按照顺序处理的,最终结果存放在 f[n%2][m],输出即可。
多重背包二进制优化
由于多重背包三重循环的时间复杂度是
二进制优化原理:
任何一个正整数都可以表示为若干个
通过二进制拆分,我们将数量为
二进制拆分示例:
假设某物品有
这样用
为什么我们可以这样拆分?
因为
二进制优化完整实现:
#include <bits/stdc++.h>
using namespace std;
int n,m;// 原始数据
vector<int> new_w,new_v; // 存储拆分后的物品
// 这里用vector,是因为我们不需要知道二进制拆分后的数据长度是多少
int main()
{
scanf("%d%d",&n,&m);
// 二进制拆分
for (int i=0;i<n;++i){
int w,v,c;
scanf("%d%d%d",&w,&v,&c);
// 二进制拆分
for (int k=1;k<=c;k*=2){
if (k*w>m) break; // 拆分出的单件物品已超总容量,无法放入,停止拆分
new_w.push_back(w*k);
new_v.push_back(v*k);
c-=k;
}
if (c>0){
new_w.push_back(w*c);
new_v.push_back(v*c);
}
}
// 转换为01背包
vector<int>f(m+5,0);
int new_n=new_w.size();
//按照01背包的步骤来做,只是替换为新的物品数量、价值和重量
for (int i=0;i<new_n;++i){
for (int j=m;j>=new_w[i];--j){
f[j]=max(f[j],f[j-new_w[i]]+new_v[i]);
}
}
printf("%d",f[m]);
return 0;
}
时间复杂度优化为
当然,在二进制拆分后,你也可以按照 01 背包的二维数组、滚动数组进行计算。此处不再展开。
多重背包总结
| 实现方法 | 时间复杂度 | 空间复杂度 | 缓存友好度 | 代码复杂度 | |
|---|---|---|---|---|---|
| 朴素 DFS | 差 | 极简 | |||
| 记忆化搜索 | 差 | 差 | 较复杂 | ||
| 二维数组 | 一般 | 简单 | |||
| 滚动数组 | 较高 | 较高 | 中等 | ||
| 二进制优化 | 高 | 复杂 |
提示:
- 二进制优化的时间复杂度中的
\sum\log c_i 表示拆分后的物品总数。
与其他背包的关系:
- 当所有
c_i=1 时:退化为 01 背包。 - 当所有
c_i\geqslant\left\lfloor\dfrac m{w_i}\right\rfloor 时:退化为完全背包。 - 二进制优化后:转化为 01 背包问题。
- 总结:01 背包和完全背包只是多重背包的特殊情况。
推荐练习题目:
- 洛谷 P1776 宝物筛选 - 多重背包模版
- 洛谷 P2347 砝码称重 - 多重背包应用
- 洛谷 P1782 旅行商的背包 - 多重背包+01 背包
- 洛谷 P1833 樱花 - 混合背包(含多重背包)
- 洛谷 P2347 砝码称重 - 多重背包应用
适用场景建议:
- 理解阶段:DFS 和记忆化搜索必须掌握。
- 学习阶段:基础解法,理解多重背包本质。
- 竞赛实战:二进制优化,平衡效率与实现难度。
在绝大多数竞赛场景中,二进制优化已完全够用,且更容易实现、调试。
单调队列优化是理论上的最优解,体现了算法设计的精妙。掌握它可以帮你:- 解决极特殊的数据约束。
- 更深刻地理解动态规划与数据结构结合的优化范式。
- 为学习更复杂的斜率优化等高级 DP 打下坚实基础。
::::warning[易错点]
- 记忆化搜索或朴素 DFS 未判断
k\leqslant c_i 的情况,导致退化为完全背包。 - 记忆化搜索或朴素 DFS 由于
n 过大,导致栈溢出,引发 RE。 - 二维数组和滚动数组第
i 行没有继承第i-1 行导致错误。 - 滚动数组误用位运算导致结果错误。
- 二进制拆分后,问题已转化为对若干独立新物品的01背包问题,因此内层循环必须使用倒序枚举
j ,这与 01 背包的要求完全一致。 :::warning[单调队列优化易错点详解] - 初始化与状态保护:处理每一种新物品前,务必使用
g=f (或memcpy)将代表上一轮结果的状态数组f 完整拷贝至备份数组g 。因为按余数分组滚动更新时,f 数组会被直接覆盖,必须用上一轮的干净状态g 进行转移计算。 - 队列元素的本质:单调队列
q 中存储的是商a ,而非背包容量索引j 。
两者的关系由j=r+aw 定义,其中r=j\bmod w 是余数。理解此映射是掌握算法的核心。 - 窗口失效的边界条件:判断队首决策是否过期(即选择物品个数超出限制
c )的条件是a-q_{\text{front}}>c 。这里使用严格大于,是因为窗口[a-c, a] 需要恰好包含c+1 种决策(对应选0\sim c 个物品)。若误用\geqslant 会导致可选项减少。 - 单调性的维护依据:在将当前状态(商
a )加入队尾时,需确保队列的价值单调递减。比较的依据是归一化价值:g[j]-av 。弹出队尾的条件是g[r+q_{\text{back}}\cdot w]-q_{\text{back}}\cdot v\leqslant g[j]-av 。 ::: ::::结语
本文介绍了01 背包、完全背包、多重背包的 DFS 暴力搜索到动态规划,由浅入深,非常适合刚刚入门背包问题的新手学习。
其中,DFS 能够帮助初学者理解背包问题的本质,从而更好地学习背包优化、背包变种问题。
致谢与创作谈:
-
感谢各位 OIer 的阅读。为求表达更流畅,本文使用了 DeepSeek 进行语句润色,但全文的知识脉络、代码实践、教学设计,皆出自我个人的学习沉淀。
-
我是怎么写这篇文章的? 我没有打草稿的习惯,习惯在脑子里把一个问题彻底想穿,同时边写边想:从 DFS 的递归树长啥样,到
f_{i,j} 这个状态到底在指什么,再到为什么j 要倒着遍历——全都想明白了,才打开编辑器一气呵成,写完后进行改动。
在最开始的时候,这篇文章只有 DP 的内容;后来,我加上 DFS 和记忆化搜索,变得更符合初学者的学习路线,同时加上优秀作品,从10000 字到了30000 字。
因此,你看到的文章结构,就是我去年啃背包时大脑中的思考流程图。文中的每个“易错点”,都是我提交记录里真实的 WA 与 RE 换来的教训。 -
对于本文中的所有代码,均在洛谷题目测试通过。由于部分题目的测试点数据量过多,因此 DFS、记忆化搜索、二维数组等未能全部通过测试点,因此仅能按照部分分来验证我的代码是否准确。
-
特别感谢《信息学奥林匹克竞赛实战笔记 B 册》(浙江大学出版社)这部指导书,作者在学习 DP 的时候就用到这本书,收获颇多。
-
由于作者水平有限及时间仓促,文中如有疏漏之处,欢迎批评指正和评论该文章。
-
多重背包还存在一种更极致的优化方法——单调队列优化,能将时间复杂度优化至
O(nm) 。因其实现较为复杂,且多用于对性能要求极高的场景,本文暂不展开。待读者牢固掌握前述所有基础后,可自行探索这一精妙的算法(可看后面推荐的文章)。 -
作者一般节假日和工作日晚上
9 点以后在线,会尽快处理各位读者的评论,若您的评论长时间未得到处理,请耐心等待。 -
希望您能够在这篇文章了解背包、学习背包、深悟背包。
- 如果你是蒟蒻,你可以从这篇文章学习到完整的学习路径的背包 DP。
- 如果你是神犇,你可以通过这篇文章来回顾自己的背包 DP。
洛谷其他背包 DP 优秀文章
-
背包问题 (附单调队列优化多重背包)(顾z 著)
评价:2018 年9 月13 日发表,品质值得信赖,内容丰富,评论较多。 -
背包问题(weichenglu 著)
评价:2025 年10 月21 日发表,品质优良,时效性强。 -
背包问题模板(残阳如血 著)
评价:2025 年1 月31 日发表,图文并茂,解答详细。 -
题解:Linear Inequation(Wxb2010 著)
评价:2025 年12 月16 日发表,解法新颖,极为详细。站外背包 DP 优秀作品
-
错误的直觉!如何用有限的容量装下尽可能高价值的物品?(NotOnlySuccess)
-
动态规划背包问题汇总(T_zhao)
常见问题解答
Q:为什么我复制你的代码后输出不正确?你不是说都测试过吗?
A:因为有些题目输入的顺序不同,以洛谷 P1048 采药为例,我的代码是先输入n (物品总数),再输入m (背包容量),但是很明显,这道题是反过来的。
我的建议:要深刻理解背包的思想,自己“手搓”出来的代码才是最真实的,最符合实际题目的。
Q:看完文章我还是不懂,是不是我太笨了?
A:绝对不是! 我当初学背包时,每个阶段都卡过:
- DFS 觉得这有什么用
- 记忆化搜索觉得好麻烦
- 二维 DP 觉得公式好复杂
- 降维优化觉得为什么
j 要倒着遍历。
我的建议:不要试图一次全懂。你可以这样:
- 今天看懂 DFS,写几道题
- 明天再看记忆化搜索
- 再过几天看二维 DP
给自己至少两周的时间让这些概念在脑子里沉淀。动态规划是需要反刍的知识。
Q:为什么提交 DFS 或记忆化搜索代码会出现 RE?
A:这通常是由于递归深度过大导致栈溢出。系统递归栈的深度有限,当物品数
Q:多重背包的二进制优化为什么有效?
A:其有效性基于一个数学事实:任何正整数都可以由若干个
Q:背包问题中最容易忽略的关键点是什么?
A:初始化和状态定义的语义。这直接对应了问题的两种不同要求(以二维数组为例):
- 不要求装满:整个
f 为0 。含义是任何容量的背包,在不装任何物品时价值为0 ,且合法。
这个时候,结果存储在f_{n,m} 。 - 要求恰好装满:
f_{0,0}=0 ,其余设为-\infty 。含义是只有容量为0 的背包能被恰好装满,其他初始状态都不可达。-\infty 保证了所有有效状态都必须从f_0 转移而来,从而确保背包被装满。
如果题目要求恰好装满,答案就在f_{n,m} ;如果f_{n,m}=-\infty ,则无法将背包恰好装满。
如果题目不要求装满,答案在f_{n,1},f_{n,2},\dots,f_{n,m-1},f_{n,m} 之中取最大值。版权声明
本文采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
您需遵守下列条件:
- 署名:您必须给出适当的署名,提供指向本许可协议的链接,并标明是否对原始作品作了修改。您可以用任何合理的方式来署名,但不得以任何方式暗示许可人为您或您的使用背书。
- 非商业性使用:您不得将本作品用于商业目的。
- 相同方式共享:如果您再混合、转换或者基于本作品进行创作,您必须基于与原先相同的许可协议分发您贡献的作品。
最后,欢迎各位读者引用本文(需注明文章来源和署名),祝您背包 DP 之路一帆风顺!