背包问题:
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weighti,得到的价值是valuei 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

我们需要使用二维数组,为什么呢?
因为有两个维度需要分别表示:物品 和 背包容量
如图,二维数组为 dpij。

那么这里 i 、j、dpij 分别表示什么呢?
i 来表示物品、j表示背包容量。

背包容量为 0,放不下物品0 或者物品1,此时背包里的价值为0。
背包容量为 1,只能放下物品0,背包里的价值为15。
背包容量为 2,只能放下物品0,背包里的价值为15。
背包容量为 3,上一行同一状态,背包只能放物品0,这次也可以选择物品1了,背包可以放物品1 或者 物品0,物品1价值更大,背包里的价值为20。
背包容量为 4,上一行同一状态,背包只能放物品0,这次也可以选择物品1了,背包可以放下物品0 和 物品1,背包价值为35。
dp14 表示什么意思呢。
任取 物品0,物品1 放进容量为4的背包里,最大价值是 dp14。
通过这个举例,我们来进一步明确dp数组的含义。
即dpij 表示从下标为0-i的物品里任意取,放进容量为j的背包,价值总和最大是多少。
两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值)
dp[1][4] = max(dp[0][4], dp[0][1] + 物品1 的价值)
以上过程,抽象化如下:
-
不放物品i:背包容量为j,里面不放物品i的最大价值是dpi - 1j。
-
放物品i:背包空出物品i的容量后,背包容量为j - weighti,dpi - 1j - weight\[i] 为背包容量为j - weighti且不放物品i的最大价值,那么dpi - 1j - weight\[i] + valuei (物品i的价值),就是背包放物品i得到的最大价值
递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
一维:
dpj表示:容量为j的背包,所背的物品价值可以最大为dpj,那么dp0就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dpj = max(dpj, dpj - weight\[i] + valuei);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
只能从后向前遍历,不然会出现物品0就会被重复加入多次
-
第一步:算
j = 2公式:
dp[2] = max(dp[2], dp[2 - 1] + 15) = max(0, dp[1] + 15)关键来了: 此时的
dp[1]是多少?因为是倒序,dp[1]还没有 被这一轮更新过,它依然保留着初始状态的 0 。所以:
dp[2] = max(0, 0 + 15) = 15。✅ 含义:用容量2的背包装物品0,价值是15(剩下的1容量空着)。
-
第二步:算
j = 1公式:
dp[1] = max(dp[1], dp[1 - 1] + 15) = max(0, dp[0] + 15)dp[0]永远是0。所以:
dp[1] = 15。✅ 含义:用容量1的背包装物品0,价值是15。
最终结果: dp[2] = 15。这完全正确,因为自始至终只有 1 个物品。
场景设定(2个物品,背包容量为4)
| 物品 | 重量 | 价值 |
|---|---|---|
| 物品0 | 2 | 10 |
| 物品1 | 1 | 1 |
现实世界的正确答案 :容量为4,最佳方案是同时拿物品0 + 物品1 (总重量 2+1=3 ≤ 4),总价值 = 10 + 1 = 11。
处理物品0(重量2,价值10)------ 倒序
-
算
j=4:dp[4] = max(0, dp[2]+10)。此时dp[2]还没被更新 (还是0),所以dp[4]=10。 -
算
j=3:dp[3] = max(0, dp[1]+10) = 10。 -
算
j=2:dp[2] = max(0, dp[0]+10) = 10。
此时 dp 数组状态:[0, 0, 10, 10, 10]
✅ 关键:容量4只装了物品0一次,价值是10(剩下2容量空着,但物品0不能再用了)。
2. 处理物品1(重量1,价值1)------ 倒序
-
算
j=4:dp[4] = max(现有10, dp[3]+1)。注意,
dp[3]还是上一轮(没选物品1时)的旧数据,等于 10 。所以
10 + 1 = 11,dp[4]被更新为 11。 -
算
j=3:dp[3] = max(现有10, dp[2]+1=11) = 11。 -
算
j=2:dp[2] = max(10, dp[1]+1=1) = 10。 -
算
j=1:dp[1] = 1。
最终答案:11。完全正确!物品0和物品1各拿了一次
目标和:
5步走:
1.dp【i】【j】表达的是用0-i的物品填满容量为j的书包有几种方法。
情况一:if (nums[i] > j) ------ 装不下
当前数字太大,超过了目标容量 j。
-
决策 :别无选择,只能不选它。
-
结果 :方法数完全继承上一行的结果,即
dp[i][j] = dp[i - 1][j]。
情况二:else ------ 装得下
当前数字小于等于目标容量 j。
-
决策 :此时你有两种互斥的选择,总方法数等于这两种情况之和(分类加法计数原理)。
-
选择1(不选) :无视当前数字,方法数为
dp[i - 1][j]。 -
选择2(选) :决定把当前数字放进去。
既然放进去占了
nums[i]的空间,那么剩下的j - nums[i]容量,就必须由前i-1个数字 来凑。凑出剩余容量的方法数为
dp[i - 1][j - nums[i]]。 -
结果 :
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]。
3.初始化:
第一列dpi0,如果数字大于0,为0的都只有一种情况,就是不放。
第一行dp0j,如果这个j小于nums【0】,就只有一种方法就是不放,如果大于就只有零种方法。
如果nums【0】就是0呢?
如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。
- 放0件物品
- 放物品0
- 放物品1
- 放物品0 和 物品1
此时是有4种方法。
其实就是算数组里有t个0,然后按照组合数量求,即 2^t 。
初始化如下:
int numZero = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) numZero++;
dp[i][0] = (int) pow(2.0, numZero);
}
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(int n:nums){
sum+=n;
}
int left=(sum+target)/2;
if((sum+target)%2==1)return 0;
if(sum<abs(target))return 0;
vector<vector<int>>dp(nums.size(),vector<int>(left+1,0));
int numZero = 0;
dp[0][0] = 1;
if (nums[0] <= left) dp[0][nums[0]] += 1;//为什莫是+=呢?因为在0的情况下,上面等于零时因为不放他已经是1了,在这里自己相等的数加一种放入自己的情况,就在加1。
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) numZero++;
dp[i][0] = (int) pow(2.0, numZero);
}
// 初始化最上行
//
// 初始化最左列,最左列其他数值在递推公式中就完成了赋值
for(int i=1;i<nums.size();i++){
for(int j=0;j<=left;j++){
if(nums[i]>j)dp[i][j]=dp[i-1][j];
else dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]];
cout<<dp[i][j]<<" ";
}
}
return dp[nums.size()-1][left];
}
};
一和零
开始动规五部曲:
- 确定dp数组(dp table)以及下标的含义
dpij:最多有i个0和j个1的strs的最大子集的大小为dpij。
2.确定递推表达式
dpij 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。
dpij 就可以是 dpi - zeroNumj - oneNum + 1。
然后我们在遍历的过程中,取dpij的最大值。
所以递推公式:dpij = max(dpij, dpi - zeroNumj - oneNum + 1);
此时大家可以回想一下01背包的递推公式:dpj = max(dpj, dpj - weight\[i] + valuei);