01背包:经典DP问题( 基本/滚动数组(优化)/恰好装满 )

本文详细介绍了01背包问题的经典动态规划解决方案,并探讨了不同优化方法,包括使用滚动数组减少空间复杂度,以及如何进一步优化以提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        所谓01背包问题,是指对于一定数量( i )的物品有一个容量为( j )的背包,每个物品都有自己的容量( k )、价值(value)。在保证物品容量之和不大于背包容量的前提下,如何选取物品得到最大价值?注意每个物品只能取一次,所以说是每个物品不是0个就是1个,称为01背包问题。

        比如说给出最大容量是6,三个物品的容量和价值分别是2,5;3,8;4,9。则取容量是2和4的物品得到的价值最大,为14。这里用到的是经典的动态规划思想( DP )。

        分解问题,把物品一件一件的放入背包内,实时记下当前所有可能容量的最大价值。如上面的例子中,先放容量2价值5的物品,容量在2-6的所有可能放下该物品的背包都存下该物品,用数组记录dp[1][[2]~dp[1][6]的值为5。余下的dp[1][1]保持0不变。

        之后放入第二件物品,容量是3,价值是8。可能放下该物品的背包容量是3-6,此时需要判断在这些背包容量中该不该放该物品!容量是3的背包此时里面是刚才的容量为2的物品,把容量是3的物品放进去,价值会更高( 8>5 ),而容量是5和6的背包可以同时放下两个物品,最大价值就是5+8=13。用数组记录dp[2][3]~dp[2][4]的值为8,dp[2][5]~dp[2][6]的值为13。余下的dp[2][1]~dp[2][2]同dp[1][1]~dp[1][2]。

        放入最后一件物品,容量4,价值9。可能放下该物品的背包容量是4-6,同上面的方法,dp[3][4] = 9;dp[3][5] = 13;dp[3][6] = 14。( dp[2][2]+9 )

        由此可以推出,每次存放物品都需要判断当前背包容量减去当前物品容量(k)的最大价值加上当前物品的价值( value )是否大于原价值。如果大于就刷新最大值。状态转移方程dp[ind][jnd]=Max(dp[ind-1][jnd-k]+value,dp[ind-1][jnd])。这个方程用在可能放下该物品的前提下!如果当前物品容量大于当前背包容量则不计算,存之前此背包容量的价值即可。dp[ind][jnd]=dp[ind-1][jnd]

         存入物品个数( i )和总容量( j ),再得到i个物品的容量和价值后。跑一遍状态转移方程,时间和空间复杂度都是O( i*j )。

        对于最多有1000个物品,背包最大容量是10000的01背包问题。

#include<stdio.h>
#define Max(a,b) (a>b?a:b)
int dp[1005][10005];
int k[1005], value[1005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	for (int ind = 1; ind <= i; ++ind)
		scanf("%d %d", &k[ind], &value[ind]);
	for (int ind = 1; ind <= i; ++ind)
	{
		for (int jnd = 1; jnd <= j; ++jnd)
		{
			if (jnd - k[ind] < 0)
				dp[ind][jnd] = dp[ind - 1][jnd];
			else
				dp[ind][jnd] = Max(dp[ind - 1][jnd], dp[ind - 1][jnd - k[ind]] + value[ind]);
		}
	}
	printf("%d\n", dp[i][j]);
	return 0;
}

        dp数组是1005*10005,这就显得有些大了,如何优化空间复杂度呢?那就是神奇的滚动数组!上面用状态转移方程一层一层的存储下了每次放物品的所有容量的最大价值,这就会显得没有必要,因为最后需要的只有最后一行的最大容量的价值。所以说根据上一行得到下行数据后上一行的数据就是没有用处的了。于是用一个一维数组dp[10005]存储当前的各容量价值即可,每次输入一个物品后,根据它的容量和价值和dp数组历史值刷新dp数组就可以啦。

        具体是怎么个滚动法呢,得到一个输入物品容量和价值后,从最大容量背包( j )到可以放下当前物品的最小背包容量( k )依次判断,dp[ind] = Max(dp[ind-k]+value,dp[ind]) 。后面这个状态转移方程和上面的相同,但是上面的所有可能背包容量必须从最大值到最小值。这里可能很难马上理解,因为如果从最小值开始,那么比它大的背包容量就可能取到多次当前物品!比如说在还是空的的容量是10的背包里放容量价值都是1的物品,dp[1] = 1, dp[2] = dp[1] +1 = 2。显然这里出错误了,之后一直到dp[10]一直会递增下去。错误的根本原因是,从dp[2]开始状态转移方程用到的dp[ind-k]并不是历史数据了,而是放入当前物品刷新之后的dp数组。因为ind-k肯定是小于ind的,所以应该从最大值开始滚动。这样每次滚动用到的历史数据都会保证是没放入当前物品的旧数据。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Max(a,b) (a>b?a:b)
int dp[10005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	while (i--)
	{
		int k, value;
		scanf("%d %d", &k, &value);
		for (int ind = j; ind >= k; ind--)
			dp[ind] = Max(dp[ind - k] + value, dp[ind]);
	}
	printf("%d\n", dp[j]);
	return 0;
}

    使用滚动数组动态规划,时间空间复杂度都多少有所优化,空间优化最为显著,缩小到O( j )。

    **那么如何继续优化滚动数组呢?每一个物品的"滚动"都是背包最大容量到能装下物品的最小容量( k ),这会不会有冗余部分呢?考虑一下,在后面物品用不到之前滚动的历史数据时,之前滚动的时候就没必要滚动那么多了吧。用到的历史数据就是dp[ind-k]项,取极限情况的时候,后面所有物品的容量加起来一同放进背包时,背包剩余的容量( x )大于最小容量( k )。这种情况下当前物品的滚动就不必到 k 了,而应该取x。所以说优化后的滚动数组应用Max(k,x)代替k,在背包总容量较大时,优化会很显著,缺点是不能在输入的同时滚动了,因为需要待全部物品输入完毕后才能得到相应的x值。

    简单的用数据说明一下,如果背包总容量是10,当前物品的容量是5,那么没有优化前的滚动数组应该从dp[10]~dp[5]。而后面的物品分别是容量为1和2,加起来总容量不过是3,当把容量是3的物品放进背包时,就需要判断到底值不值,用到了dp[10-3]=dp[7]的数据,那么dp[7]将是后面物品可能用的到的最小背包容量,也就是说后面可能用的到的只有dp[7]~dp[10]。所以经过优化当前容量为5的物品滚动数组的变化是dp[10]~dp[7]。

    透彻明白DP是怎么解决背包问题的之后,理解这个优化方法应该比较轻松。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Max(a,b) (a>b?a:b)
int dp[10005];
int k[1005], value[1005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	int s = 0;
	for (int ind = 1; ind <= i; ind++)
	{
		scanf("%d %d", &k[ind], &value[ind]);
		s = s + k[ind];
	}
	for (int ins = 1; ins <= i; ins++)
	{
		s -= k[ins];
		for (int ind = j; ind >= Max(j - s, k[ins]); ind--)
			dp[ind] = Max(dp[ind - k[ins]] + value[ins], dp[ind]);
	}
	printf("%d\n", dp[j]);
	return 0;
}

    s 是当前物品之后所有物品的容量之和,j-s 就是前面提到的 x 。

    **进一步提出问题,要求在选取的物品容量之和等于背包总容量的前提下,得到最大价值。这就需要筛选掉背包内有空余容量的所有情况了,在现有解法的基础上只需要将dp[1]~dp[j]的初始值设定成无限小就可以了。在放第一个物品进背包的时候因为只有dp[0]的初始值是 0 ,所以只有恰好能放下这个物品的背包才会存下这个物品,之后每次放新物品都只能是之前放过物品的容量和恰好能放下当前物品容量的背包才会有效判断是否该刷新最大价值。

    

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define Max(a,b) (a>b?a:b)
int dp[10005];
int k[1005], value[1005];
int main(int argc, char* argv[])
{
	int i, j;
	scanf("%d %d", &i, &j);
	memset(dp + 1, -1, j * sizeof(int));
	int s = 0;
	for (int ind = 1; ind <= i; ind++)
	{
		scanf("%d %d", &k[ind], &value[ind]);
		s = s + k[ind];
	}
	for (int ins = 1; ins <= i; ins++)
	{
		s -= k[ins];
		for (int ind = j; ind >= Max(j - s, k[ins]); ind--)
			if (dp[ind - k[ins]] != -1)
				dp[ind] = Max(dp[ind - k[ins]] + value[ins], dp[ind]);
	}
	printf("%d\n", dp[j]);
	return 0;
}

    用 -1 表示无穷小,当用到的历史数据 dp[ind-k] 是 -1 时直接跳过。

<think>我们正在讨论01背包问题。根据引用[2]中的描述,01背包问题的特点是:数组中的元素只能选择一次;每个元素面临被选择或者不被选择的处境;选出来的元素总和要等于所有元素总和的一半(在分割等和子集问题中)。但01背包问题更一般的形式是:给定物品的重量和价值,在不超过背包容量的情况下,选择物品使得总价值最大。 注意事项和实现要点如下: 1. **问题转化**:识别问题是否属于01背包模型。通常,问题需要满足:每个物品只有选或不选两种状态(即0或1),且不能重复选取(引用[2]中提到“数组中的元素只能选择一次”)。 2. **状态定义**:经典01背包使用二维DP数组`dp[i][j]`表示前`i`个物品在背包容量为`j`时能获得的最大价值。其中`i`表示物品索引(从1开始),`j`表示背包当前容量。 3. **状态转移方程**: - 不选第`i`个物品:`dp[i][j] = dp[i-1][j]` - 选第`i`个物品(需满足`j >= weight[i-1]`):`dp[i][j] = dp[i-1][j - weight[i-1]] + value[i-1]` - 取两者最大值:`dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i-1]] + value[i-1])` 4. **初始化**: - 容量为0时,最大价值为0:`dp[i][0] = 0`(对于所有`i`) - 没有物品时,任何容量下的价值都是0:`dp[0][j] = 0`(对于所有`j`) - 注意:如果题目要求恰好装满,则初始化时`dp[0][0]=0`,而`dp[0][j]`(当`j>0`)应设为负无穷(表示不可达)。但一般情况下(不需要恰好装满),初始化为0即可。 5. **空间优化**(滚动数组): - 由于`dp[i][j]`只依赖于`dp[i-1][...]`,因此可以压缩为一维数组:`dp[j]`表示容量为`j`时的最大价值。 - 此时需要**反向遍历容量**(从大到小),以确保每个物品只被计算一次(引用[2]中提到了空间优化)。 - 状态转移方程变为:`dp[j] = max(dp[j], dp[j - weight[i-1]] + value[i-1])`,其中`j`从最大容量递减到`weight[i-1]`。 6. **遍历顺序**: - 外层循环遍历物品(从0到n-1),内层循环遍历容量(从大到小,直到当前物品的重量)。 7. **边界处理**: - 注意物品索引和容量索引的边界,避免数组越界。 - 在空间优化的一维DP中,内层循环的终止条件可以是当前物品的重量(因为小于该重量的容量无法放入该物品)。 8. **问题变种**: - 如引用[2]中的分割等和子集问题,可以转化为背包容量为总和一半,每个物品重量等于其数值,价值也等于其数值,然后判断最大价值是否等于容量(即是否能恰好装满)。 - 其他变种:求方案数、恰好装满的最大价值等。 9. **时间复杂度和空间复杂度**: - 时间复杂度:$O(n \times W)$,其中n是物品数量,W是背包容量。 - 空间复杂度:未优化时为$O(n \times W)$,优化后为$O(W)$。 10. **注意事项**: - 确保物品索引和重量数组、价值数组对应正确。 - 在空间优化时,内层循环必须反向遍历,否则会变成完全背包(每个物品可以选多次)。 下面给出一个简单的01背包代码示例(空间优化版本):</think>### 01背包问题的注意事项与实现要点 01背包问题的核心是**在有限容量下选择物品使总价值最大**,每个物品只能选或不选(0/1决策)。以下是关键注意事项和实现要点(参考引用[2]的转化逻辑): --- #### 一、核心注意事项 1. **问题转化识别** - 当问题满足以下条件时,可转化为01背包模型: - 物品**只能选或不选**(不可拆分)[^2] - 存在**容量限制**(如背包容量、总和限制) - 目标为**最大化价值**或**满足特定条件**(如等和子集分割)[^2] 2. **边界条件处理** - **容量为0**:价值始终为0(无物品可装) - **物品重量为0**:需根据价值是否非负决定是否选择 - **负权重/价值**:需特殊处理(如转化为非负值或调整状态转移) 3. **空间优化风险** - 使用滚动数组优化时,**内层循环需倒序遍历容量**(从大到小),避免重复计算(完全背包问题才正序遍历)[^1][^2] - 若需记录具体方案,不能直接压缩为一维数组 --- #### 二、实现要点 1. **状态定义** - `dp[i][j]`:前 `i` 个物品在容量 `j` 下的最大价值 - **初始化**:`dp[0][j] = 0`(无物品时价值为0) 2. **状态转移方程** $$ dp[i][j] = \max \begin{cases} dp[i-1][j] & \text{不选第 } i \text{ 个物品} \\ dp[i-1][j - w_i] + v_i & \text{选第 } i \text{ 个物品} \quad (j \geq w_i) \end{cases} $$ 3. **空间优化(一维数组)** ```python dp = [0] * (capacity + 1) # 初始化一维DP数组 for i in range(n): for j in range(capacity, weights[i] - 1, -1): # 倒序遍历容量 dp[j] = max(dp[j], dp[j - weights[i]] + values[i]) ``` 4. **恰好装满问题** - 要求容量**恰好用尽**时,初始化: - `dp[0] = 0`(容量0时价值0) - `dp[j] = -∞`(`j > 0`,表示不可达) --- #### 三、常见错误与规避 | 错误类型 | 后果 | 规避方法 | |-------------------|---------------------|------------------------------| | 正序更新一维DP | 物品被重复选择 | 内层循环**倒序**遍历容量 | | 忽略负数权重 | 状态转移失效 | 预处理数据或调整状态定义 | | 未处理边界容量 | 数组越界 | 循环条件:`j >= weights[i]` | | 混淆01/完全背包 | 结果错误 | 确认物品是否**唯一** | --- #### 四、实战示例(Python) ```python def knapsack_01(weights, values, capacity): n = len(weights) dp = [0] * (capacity + 1) for i in range(n): # 倒序遍历容量:从大到小 for j in range(capacity, weights[i] - 1, -1): if j >= weights[i]: dp[j] = max(dp[j], dp[j - weights[i]] + values[i]) return dp[capacity] # 测试 weights = [2, 3, 4, 5] values = [3, 4, 5, 6] capacity = 8 print(knapsack_01(weights, values, capacity)) # 输出:10(选择物品2和4) ``` --- #### 五、扩展:01背包变种 1. **分割等和子集**(LeetCode 416) - 转化:背包容量 = 数组和的一半,物品价值 = 物品重量[^2] - 目标:判断是否能恰好装满背包 2. **目标和**(LeetCode 494) - 转化:背包容量 = (sum(S) + target) / 2 - 目标:求恰好装满的方案数 --- **相关问题** 1. 如何将完全背包问题转化为01背包? 2. 01背包问题的时间复杂度能否优化到低于 $O(n \cdot W)$? 3. 如何处理物品重量为负数的背包问题? 4. 动态规划与记忆化搜索在背包问题中的优劣比较? [^1]: 完全背包01背包的转化关系 [^2]: 01背包模型的核心特征与问题转化逻辑
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值