0-1 背包问题
0-1 背包问题
一、典型题目
n种物品,每种只有1个。( 因此称为0-1背包问题——对应着每个物体 选与不选 的两种选择)
第 i 种物品的体积为
,重量为 。 背包容量为 V 。
选物品装到背包,使得背包内的物品在总体积不超过C的前提下重量尽量大。
二、暴力破解的尝试
如果采用暴力破解,用回溯法,那么时间复杂度将会为 O(
三、二维数组的尝试
1、动态规划的核心是状态和状态转移方程
1.1、状态
f [ i ] [ j ] ——定义:前 i个物品,背包容量 j下的最优解
我们据此可以发现一个关系——当前的状态是依赖于之前的状态的;同时,当前的状态是在之前的状态的基础上来选择 对于当前的物品要不要选的 两种决定。
换言之,对于f [i] [j],可以由两个方面来做决定,(假设容量够)在f[i-1] [ j ]的基础上,①不要当前的第i个,②要当前的第i个。这就引出了状态转移方程。
1.2、状态转移方程
(1)容量不够:那么 f[ i ] [ j ]=f[i-1] [j]
(2)容量够:
①不要当前的。那么 f[ i ] [ j ]=f[i-1] [j]
;
②要当前的。那么 f[i] [j] = f[i - 1] [ j - v[i] ] + w[i]
(对于②为什么? 当我们考虑当前的状态是,我们就需要考虑“刚才那个阶段的状态”,也就是说:我们想要在当前放入v[i] (注意 此刻的 最大容量记为了 j ),就需要要求前一个的 最大值 得是 j - v[i] ,于是我们得出来了前一个的基础为f[i - 1] [ j - v[i] ],此时的价值在原基础上加上i的价值(w[i])即可)
综上,我们就可以来决定当前的最优解怎么选了——选或者不选当前的第i个比较后取最大值,f[i] [j]=max (f[i-1] [j] , f[i-1] [j-w[i]]+v[i] )
2、代码
...
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
// 容量不够用
if(j < v[i]) f[i][j] = f[i - 1][j];
// 容量够用
else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
...
四、二维下的一些考虑的问题
1、f [MAX] [MAX] 初始化——全为“0”(零)
f[0] [0]=0 ,背包容量为0,什么也装不下,同理f[i] [0]=0;
与之对应的,所选物品数量为零,什么也不装,f[0] [i]=0;
由此,全部赋值为0。
2、二维数组的利用率
假设(后续表格展示的前提假设亦是如此):
背包容量max=4;
物品1--重量1 价值 15;
物品2--重量3 价值 20;
物品3--重量4 价值 30;
则:
物品 i \ 容量 j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 15 | 15 | 15 | 15 |
2 | 0 | 15 | 15 | 20 | 35 |
3 | 0 | 15 | 15 | 20 | 35 |
我们发现:每个“阶段”所用的只是当前的和“上一轮的”,于是就会出现可以 压缩空间 的问题,就有了优化方案——滚动数组
五、基于二维数组的优化——一维数组的尝试
其实,我们只需要简单的做个等价变形即可:
for(int i = 1; i <= n; i++)
for(int j = m; j >= 0; j--) //改为 逆序 枚举
{
if(j < v[i])
// f[i][j] = f[i - 1][j]; // 优化前
f[j] = f[j]; // 优化后
else
// f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 优化前
f[j] = max(f[j], f[j - v[i]] + w[i]); // 优化后
}
于是:
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
f [j]表示:容量为j的背包,所背的物品价值可以最大为f [j]。
六、一维数组尝试中的思考
1、为什么 j 采用逆序:
①比较优化前后:如果不逆序,优化后的f[j - v[i]]指的是当前的f[i] [j - v[i]],而非我们所期望的f[i - 1] [j - v[i]];
②顺序枚举可能会导致重复装入:我们通过二维数组的尝试可以知道,第i个是依赖于第i-1个的——于是:如果采用正序,当i=1计算完后,数组内会存有值,当i=2时,会在此基础上再从j=0进行判断,此时就有可能会再加入一次之前已经加过的值(而0-1背包只能加一次);但是采用逆序的话,则不会存在重合的状态——综合来看问题就在于:当遍历 j 到一半(或者一部分)时,此时数组内存在着两个“阶段”的数据,而使用数据都是后面位置用前面位置的,要想互不影响,只好从后面开始遍历,这样是用的前面位置的数据就会是“上一个阶段”的。(同时,这也解释了为什么二维数组的尝试不需要逆序遍历——每“层”的(每个“阶段”的,对应上述文本中的表格一行)数据都是同一个“阶段”的,不会被覆盖)
2、为什么一位数组的尝试只能先遍历物品再遍历容量:
关键在于逆序遍历的写法。如果如果不是 先遍历物品再遍历容量,而是反过来的话,就会让每次遍历f[j](容量为j的背包,所背的物品价值可以最大为f [j])只能放一个物品(背包只放了一个物品)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?