01背包问题的空间优化与边界处题目解析
01背包问题是经典的动态规划问题,旨在选择若干物品装入背包,使得总价值最大且不超过背包容量。每个物品只能选或不选(0或1),不可分割。
选和不选是01背包问题最大的特征
例题
01背包
题目链接:
01背包
要点:
老师代码:
#include <iostream>
#include <string.h>using namespace std;
const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];int main()
{// 读⼊数据cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];// 解决第⼀问for(int i = 1; i <= n; i++)for(int j = 0; j <= V; j++) // 修改遍历顺序{dp[i][j] = dp[i - 1][j];if(j >= v[i])dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);}cout << dp[n][V] << endl;// 解决第⼆问memset(dp, 0, sizeof dp);for(int j = 1; j <= V; j++) dp[0][j] = -1;for(int i = 1; i <= n; i++)for(int j = 0; j <= V; j++) // 修改遍历顺序{dp[i][j] = dp[i - 1][j];if(j >= v[i] && dp[i - 1][j - v[i]] != -1)dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);}cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;return 0;
}
空间优化:
#include <iostream>
#include <string.h>
using namespace std;const int N = 1010;
int n, V, v[N], w[N];
int dp[N];int main()
{// 读⼊数据cin >> n >> V;for(int i = 1; i <= n; i++)cin >> v[i] >> w[i];// 解决第⼀问for(int i = 1; i <= n; i++)for(int j = V; j >= v[i]; j--) // 修改遍历顺序dp[j] = max(dp[j], dp[j - v[i]] + w[i]);cout << dp[V] << endl;// 解决第⼆问memset(dp, 0, sizeof dp);for(int j = 1; j <= V; j++) dp[j] = -1;for(int i = 1; i <= n; i++)for(int j = V; j >= v[i]; j--)if(dp[j - v[i]] != -1)dp[j] = max(dp[j], dp[j - v[i]] + w[i]);cout << (dp[V] == -1 ? 0 : dp[V]) << endl;return 0;
}
老师思路:
第一问:
-
状态表⽰:
dp[i][j]
表⽰:从前 i 个物品中挑选,总体积「不超过」 j ,所有的选法中,能挑选出来的最⼤价值 -
状态转移⽅程:线性 dp 状态转移⽅程分析⽅式,⼀般都是根据「最后⼀步」的状况,来分情况讨论:
- i. 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j 。此时
dp[i][j] = dp[i - 1][j]
; - ii. 选择第 i 个物品:那么我就只能去前 i - 1 个物品中,挑选总体积不超过
j - v[i]
的物品。此时dp[i][j] = dp[i - 1][j - v[i]] + w[i]
。但是这种状态不⼀定存在,因此需要特判⼀下。 - 综上,状态转移⽅程为:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])
。
- i. 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j 。此时
-
初始化:我们多加⼀⾏,⽅便我们的初始化,此时仅需将第⼀⾏初始化为 0 即可。因为什么也不选,也能满⾜体积不⼩于 j 的情况,此时的价值为 0 。
-
填表顺序:根据「状态转移⽅程」,我们仅需「从上往下」填表即可。
-
返回值:根据「状态表⽰」,返回
dp[n][V]
。
第二问
第⼆问仅需微调⼀下 dp 过程的五步即可。 因为有可能凑不⻬ j 体积的物品,因此我们把不合法的状态设置为 -1 。
-
状态表⽰:
dp[i][j]
表⽰:从前 i 个物品中挑选,总体积「正好」等于 j ,所有的选法中,能挑选出来的最⼤价值。 -
状态转移⽅程:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])
。但是在使⽤dp[i - 1][j - v[i]]
的时候,不仅要判断 j >= v[i] ,⼜要判断dp[i- 1][j - v[i]]
表⽰的情况是否存在,也就是dp[i - 1][j - v[i]] != -1
-
初始化:我们多加⼀⾏,⽅便我们的初始化:i. 第⼀个格⼦为 0 ,因为正好能凑⻬体积为 0 的背包; ii. 但是第⼀⾏后⾯的格⼦都是 -1 ,因为没有物品,⽆法满⾜体积⼤于 0 的情况
-
填表顺序:根据「状态转移⽅程」,我们仅需「从上往下」填表即可。
-
返回值:由于最后可能凑不成体积为 V 的情况,因此返回之前需要「特判」⼀下。
我的代码:
#include <iostream>
#include <vector>
using namespace std;int main()
{int n, v;cin >> n >> v;vector<int> sz(n + 1);vector<int> val(n + 1);for(int i = 1; i <= n; i++) cin >> sz[i] >> val[i];//状态表示vector<vector<int>> dp1(n + 1, vector<int>(v + 1));auto dp2 = dp1;//初始化for(int j = 1; j <= v; j++) dp2[0][j] = -1;//使空位置的物品价值等于j是不存在的,而不是物品价值为0,所以填入-1//填表for(int i = 1; i <= n; i++){for(int j = 0; j <= v; j++){dp1[i][j] = dp1[i - 1][j];if(sz[i] <= j) dp1[i][j] = max(dp1[i][j], dp1[i - 1][j - sz[i]] + val[i]);dp2[i][j] = dp2[i - 1][j];if(sz[i] <= j && dp2[i - 1][j - sz[i]] != -1) dp2[i][j] = max(dp2[i][j], dp2[i - 1][j - sz[i]] + val[i]);}}cout << dp1[n][v] << endl;cout << (dp2[n][v] == -1 ? 0 : dp2[n][v]) << endl;return 0;
}
我的思路:
老师教的
对于第一问:
-
状态表示:
dp[i][j]
表示 i 位置之前的物品能够装入背包体积不大于 j 的最大价值 -
状态转移方程:
-
如果不选 i 位置的物品 就要从 i - 1位置的物品中选择能够装入背包体积不大于 j 的最大价值
dp[i][j] = dp[i - 1][j];
-
如果选 i 位置 的物品 就要判断能不能选,如果不能就选择前面的方案,如果能选,则我们选上当前位置的物品
val[i]
之后还要从i - 1
位置的物品中选择体积不超过j - sz[i]
的最大价值,最后吧这一种方案与前面一种方案取最大值即可if(sz[i] <= j) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - sz[i]] + val[i]);
-
对于第二问:
-
状态表示:
dp[i][j]
表示位置之前的物品能够装入背包体积刚好等于 j 的最大价值 -
状态转移方程:
-
如果不选 i 位置的物品, 我们就要从 i - 1 位置中选出装入背包体积等于 j 的最大价值
dp[i][j] = dp[i - 1][j]
这和之前的状态转移方程是一样的,因为状态表示变了 -
如果选 i 位置的物品,我们仍然要判断能不能选,此时还有一种情况,就是存不存在可以选 i 位置物品的情况,因为如果要使体积刚好等于 j 这种情况可能是不存在的,所以要多加一个判断条件
if(sz[i] <= j && dp2[i - 1][j - sz[i]] != -1) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - sz[i]] + val[i])
-
注意:dp2[i - 1][j - sz[i]] != -1
用-1判断是因为我们需要与0的意义区分开,0表示dp[i][j]
表示位置之前的物品能够装入背包体积刚好等于 j 的最大价值为0,而不是没有这种情况,只是价值为0而已
我的笔记:
-
还是要注意数组下标的映射关系,这个比较容易出错,不管是在输入数据、初始化、填表的时候
-
背包问题的思想
-
对于空间优化的建议:
- 不要深究空间优化之后状态表示以及状态转移方程的实际含义
- 空间优化重点是处理后面的填表中用不到的数据
- 注意填表方式可能不同,原因就是不能覆盖还需要的数据
分割等和子集
题目链接:
分割等和子集
要点:
- 问题核心:判断数组是否能被分割成两个和相等的子集。关键在于找到子集和为总和的一半。
- 数学条件:若总和为奇数,直接返回
false
;若最大元素超过总和一半,也直接返回false
。 - 动态规划定义:
dp[i][j]
表示前i
个元素是否能选出和为j
的子集。
老师代码:
class Solution
{
public:bool canPartition(vector<int>& nums){int n = nums.size(), sum = 0;for(auto x : nums) sum += x;if(sum % 2) return false; // 如果不能平分,直接返回 falseint aim = sum / 2; // 定义⼀下⽬标值vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1)); // 建表for(int i = 0; i <= n; i++) dp[i][0] = true; // 初始化for(int i = 1; i <= n; i++) // 填表for(int j = 1; j <= aim; j++){dp[i][j] = dp[i - 1][j];if(j >= nums[i - 1])dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];}// 返回结果return dp[n][aim];}
}
空间优化:
class Solution
{
public:bool canPartition(vector<int>& nums){int n = nums.size(), sum = 0;for(auto x : nums) sum += x;if(sum % 2) return false; // 如果不能平分,直接返回 falseint aim = sum / 2; // 定义⼀下⽬标值vector<bool> dp(aim + 1); // 建表dp[0] = true; // 初始化for(int i = 1; i <= n; i++) // 填表for(int j = aim; j >= nums[i - 1]; j--) // ⼀定要注意修改遍历顺序dp[j] = dp[j] || dp[j - nums[i - 1]];// 返回结果return dp[aim];}
};
老师思路:
先将问题转化成我们「熟悉」的题型。
如果数组能够被分成两个相同元素之和相同的⼦集,那么原数组必须有下⾯⼏个性质:
- i. 所有元素之和应该是⼀个偶数;
- ii. 数组中最⼤的元素应该⼩于所有元素总和的⼀半;
- iii. 挑选⼀些数,这些数的总和应该等于数组总和的⼀半。
根据前两个性质,我们可以提前判断数组能够被划分。根据最后⼀个性质,我们发现问题就转化成了「01 背包」的模型:
- i. 数组中的元素只能选择⼀次;
- ii. 每个元素⾯临被选择或者不被选择的处境;
- iii. 选出来的元素总和要等于所有元素总和的⼀半。
其中,数组内的元素就是物品,总和就是背包。那么我们就可以⽤背包模型的分析⽅式,来处理这道题。
请⼤家注意,「不要背」状态转移⽅程,因为题型变化之后,状态转移⽅程就会跟着变化。我们要记住的是分析问题的模式。⽤这种分析问题的模式来解决问题
-
状态表⽰:
dp[i][j]
表⽰在前 i 个元素中选择,所有的选法中,能否凑成总和为 j 这个数 -
状态转移⽅程:⽼规矩,根据「最后⼀个位置」的元素,结合题⽬的要求,分情况讨论:
- i. 不选择 nums[i] :那么我们是否能够凑成总和为 j ,就要看在前 i - 1 个元素中选,能否凑成总和为 j 。根据状态表⽰,此时
dp[i][j] = dp[i - 1][j]
; - ii. 选择 nums[i] :这种情况下是有前提条件的,此时的 nums[i] 应该是⼩于等于 j 。因为如果这个元素都⽐要凑成的总和⼤,选择它就没有意义呀。那么我们是否能够凑成总和为 j ,就要看在前 i - 1 个元素中选,能否凑成总和为
j - nums[i]
。根据状态表⽰,此时dp[i][j] = dp[i - 1][j - nums[i]]
。
综上所述,两种情况下只要有⼀种能够凑成总和为 j ,那么这个状态就是 true 。因此,状态转移⽅程为:
dp[i][j] = dp[i - 1][j]
if(nums[i - 1] <= j) dp[i][j] = dp[i][j] || dp[i - 1][j -nums[i]]
- i. 不选择 nums[i] :那么我们是否能够凑成总和为 j ,就要看在前 i - 1 个元素中选,能否凑成总和为 j 。根据状态表⽰,此时
-
初始化:由于需要⽤到上⼀⾏的数据,因此我们可以先把第⼀⾏初始化。第⼀⾏表⽰不选择任何元素,要凑成⽬标和 j 。只有当⽬标和为 0 的时候才能做到,因此第⼀⾏仅需初始化第⼀个元素
dp[0][0] = true
-
填表顺序:根据「状态转移⽅程」,我们需要「从上往下」填写每⼀⾏,每⼀⾏的顺序是「⽆所谓的」。
-
返回值:根据「状态表⽰」,返回
dp[n][aim]
的值。 其中 n 表⽰数组的⼤⼩, aim 表⽰要凑的⽬标和。 -
空间优化:所有的「背包问题」,都可以进⾏空间上的优化。对于 01背包类型的,我们的优化策略是: i. 删掉第⼀维;ii. 修改第⼆层循环的遍历顺序即可。
我的代码:
class Solution {
public:bool canPartition(vector<int>& nums) {int m = nums.size();int sum = 0;for(int i = 0; i < m; i++)sum += nums[i];if(sum % 2) return false;//如果数组和为一个奇数,则他一定不能分成两个相等的整数int k = sum / 2;vector<vector<bool>> dp(m + 1, vector<bool>(k + 1));//初始化for(int i = 0; i <= m; i++) dp[i][0] = true;//填表for(int i = 1; i <= m; i++){for(int j = 0; j <= k; j++){dp[i][j] = dp[i - 1][j];if(j - nums[i - 1] >= 0) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];}}return dp[m][k];}
};
空间优化:
class Solution {
public:bool canPartition(vector<int>& nums) {int m = nums.size();int sum = 0;for(int i = 0; i < m; i++)sum += nums[i];if(sum % 2) return false;int k = sum / 2;vector<bool> dp(k + 1);//初始化dp[0] = true;//填表for(int i = 1; i <= m; i++){for(int j = k; j >= 0; j--){if(j - nums[i - 1] >= 0) dp[j] = dp[j] || dp[j - nums[i - 1]];}}return dp[k];}
};
我的思路:
- 将问题转化为“是否存在子集和为
sum/2
”,转化为01背包问题。 - 初始化
dp[0][0] = true
(空集和为0),其他dp[0][j] = false
(j > 0
时无法用空集凑出)。
我的笔记:
- 如何将这一个问题转化为01背包问题
- 数组下标映射关系
- 为什么要多开一行一列的dp空间?因为当我们不选也就是子集为空,以及和为0的情况是有研究价值的,也就是会出现这种情况的
目标和
题目链接:
目标和
要点:
- 问题转化:设正数和为
a
,负数和为b
,则a - b = target
且a + b = sum
,推导得a = (sum + target) / 2
。 - 合法性判断:若
(sum + target)
为奇数或target
绝对值超过sum
,直接返回0
老师代码:
class Solution
{
public:int findTargetSumWays(vector<int>& nums, int target){int sum = 0;for(auto x : nums) sum += x;int aim = (sum + target) / 2;// 处理⼀下边界条件if(aim < 0 || (sum + target) % 2) return 0;int n = nums.size();vector<vector<int>> dp(n + 1, vector<int>(aim + 1)); // 建表dp[0][0] = 1; // 初始化for(int i = 1; i <= n; i++) // 填表for(int j = 0; j <= aim; j++){dp[i][j] = dp[i - 1][j];if(j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];}// 返回结果return dp[n][aim];}
}
老师思路:
本题可以直接⽤「暴搜」的⽅法解决。但是稍微⽤数学知识分析⼀下,就能转化成我们常⻅的「背包模型」的问题。
设我们最终选取的结果中,前⾯加 + 号的数字之和为 a ,前⾯加 - 号的数字之和为 b ,整个数组的总和为 sum ,于是我们有:
a + b = sum
a - b = target
上⾯两个式⼦消去 b 之后,可以得到 a = (sum + target) / 2 也就是说,我们仅需在 nums 数组中选择⼀些数,将它们凑成和为 (sum + target) / 2 即可。问题就变成了 416. 分割等和⼦集 这道题。 我们可以⽤相同的分析模式,来处理这道题
-
状态表⽰:
dp[i][j]
表⽰:在前 i 个数中选,总和正好等于 j ,⼀共有多少种选法。 -
状态转移⽅程:⽼规矩,根据「最后⼀个位置」的元素,结合题⽬的要求,我们有「选择」最后⼀个元素或者「不选择」最后⼀个元素两种策略:
-
i. 不选 nums[i] :那么我们凑成总和 j 的总⽅案,就要看在前 i - 1 个元素中选,凑成总和为 j 的⽅案数。根据状态表⽰,此时
dp[i][j] = dp[i - 1
][j] ; -
ii. 选择 nums[i] :这种情况下是有前提条件的,此时的 nums[i] 应该是⼩于等于 j 。因为如果这个元素都⽐要凑成的总和⼤,选择它就没有意义呀。那么我们能够凑成总和为 j 的⽅案数,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i] 。根据状态表⽰,此时
dp[i][j] = dp[i - 1][j - nums[i]]
-
综上所述,两种情况如果存在的话,应该要累加在⼀起。因此,状态转移⽅程为:
dp[i][j] = dp[i - 1][j] if(nums[i - 1] <= j) dp[i][j] = dp[i][j] += dp[i - 1][j - nums[i- 1]]
-
-
初始化:由于需要⽤到「上⼀⾏」的数据,因此我们可以先把第⼀⾏初始化。第⼀⾏表⽰不选择任何元素,要凑成⽬标和 j 。只有当⽬标和为 0 的时候才能做到,因此第⼀⾏仅需初始化第⼀个元素
dp[0][0] = 1
-
填表顺序:根据「状态转移⽅程」,我们需要「从上往下」填写每⼀⾏,每⼀⾏的顺序是「⽆所谓的」。
-
返回值:根据「状态表⽰」,返回
dp[n][aim]
的值。 其中 n 表⽰数组的⼤⼩, aim 表⽰要凑的⽬标和 -
空间优化:所有的「背包问题」,都可以进⾏空间上的优化。对于 01背包类型的,我们的优化策略是:
- i. 删掉第⼀维;
- ii. 修改第⼆层循环的遍历顺序即可
我的代码:
class Solution
{
public:int findTargetSumWays(vector<int>& nums, int target) {int m = nums.size();int sum = 0;for(auto& n : nums) sum += n;if((sum + target) % 2) return 0;//如果是奇数,则一定不存在一种情况使int k = (sum + target) / 2;//如果k的值小于0,这种情况下是找不到的,因为题目给了nums中的数是大于0的if(k < 0) return 0;vector<vector<int>> dp(m + 1, vector<int>(k + 1));//初始化dp[0][0] = 1;//填表for(int i = 1; i <= m; i++){for(int j = 0; j <= k; j++)//这里要从0开始,因为按照题目要求,nums[i]可能为0{ dp[i][j] = dp[i - 1][j];if(j - nums[i - 1] >= 0) dp[i][j] = dp[i][j] + dp[i - 1][j - nums[i - 1]];}}return dp[m][k];}
};
空间优化:
class Solution
{
public:int findTargetSumWays(vector<int>& nums, int target) {int m = nums.size();int sum = 0;for(auto& n : nums) sum += n;if((sum + target) % 2) return 0;//如果是奇数,则一定不存在一种情况使int k = (sum + target) / 2;if(k < 0) return 0;vector<int> dp(k + 1);//初始化dp[0] = 1;//填表for(int i = 1; i <= m; i++){for(int j = k; j - nums[i - 1] >= 0; j--){dp[j] += dp[j - nums[i - 1]];}}return dp[k];}
};
我的思路:
- 使用动态规划统计组成和为
a
的方案数,状态转移方程为:
dp[j] += dp[j - nums[i]]
(若j >= nums[i]
)。 - 初始化
dp[0] = 1
,表示空集和为0的方案数为1。
我的笔记:
-
注意边界条件,包括
sum + target
的值不能为奇数(因为k涉及除法可能存在除不尽的问题),以及k的值不能小于0(nums[i]都是大于0的数) -
要看清题目要求
-
原始数组与dp表的下标映射关系
最后一块石头的重量II
题目链接:
最后一块石头的重量II
要点:
- 问题转化:将石头粉碎过程转化为对数组元素赋予正负号,使得两部分的差值最小。最终结果等价于求数组分割成两个子集的最小差值。
- 数学推导:设总和为
sum
,理想分割为两子集和接近sum/2
,差值最小为sum - 2*max_subset_sum
。 - 状态转移优化:用01背包求不超过
sum/2
的最大子集和,直接计算最终差值。
老师代码:
class Solution
{
public:int lastStoneWeightII(vector<int>& stones){// 1. 准备⼯作int sum = 0;for(auto x : stones) sum += x;int n = stones.size(), m = sum / 2;// 2. dpvector<vector<int>> dp(n + 1, vector<int>(m + 1));for(int i = 1; i <= n; i++)for(int j = 0; j <= m; j++){dp[i][j] = dp[i - 1][j];if(j >= stones[i - 1])dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] +stones[i - 1]);}// 3. 返回结果return sum - 2 * dp[n][m];}
}
老师思路:
先将问题「转化」成我们熟悉的题型。▪ 任意两块⽯头在⼀起粉碎,重量相同的部分会被丢掉,重量有差异的部分会被留下来。那就相当于在原始的数据的前⾯,加上「加号」或者「减号」,是最终的结果最⼩即可。也就是说把原始的⽯头分成两部分,两部分的和越接近越好。▪ ⼜因为当所有元素的和固定时,分成的两部分越接近数组「总和的⼀半」,两者的差越⼩。因此问题就变成了:在数组中选择⼀些数,让这些数的和尽量接近 sum / 2 ,如果把数看成物品,每个数的值看成体积和价值,问题就变成了「01 背包问题」
我的代码:
class Solution {
public:int lastStoneWeightII(vector<int>& stones) {int m = stones.size();int sum = 0;for(auto& s : stones) sum += s;int k = sum / 2;vector<vector<int>> dp(m + 1, vector<int>(k + 1));//填表for(int i = 1; i <= m; i++){for(int j = 0; j <= k; j++){dp[i][j] = dp[i - 1][j];if(j - stones[i - 1] >= 0) dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);}}return sum - 2 * dp[m][k];}
};
我的思路:
1.两个石头相撞,结果要么为x-y,要么为y-x
2.无论你怎么两两相碰,永远有的数字前为正号,有的为负号,因此你总可以把最终式化为一堆和减去另外一堆数字和
3.因此我们要找的是这个集合的两个子集之和的最小差
4.要想子集之和差最小,则两者应该尽量接近或者相等
5.这个时候我们就可以把sum/2作为背包容量,使用01背包来解题了
我的笔记:
- 若总和为奇数,
sum/2
向下取整不影响结果,因为差值只关心两部分的和差距。 - 空间优化时,内层循环需逆序遍历,防止覆盖未使用的上一状态
解决01背包问题的注意事项
C++语法细节
- 数组下标设计:
- 从
1
开始存储物品信息,避免处理i-1
的边界问题(如nums[i-1]
对应第i
个物品)。 - 二维
dp
表可优化为一维数组,但需逆序更新防止覆盖。
- 从
- 数据类型选择:
- 若结果可能较大(如目标和方案数),使用
long long
避免溢出。 vector
初始化时,默认值需根据问题设定(如-1
表示非法状态,0
或1
表示初始方案数)。
- 若结果可能较大(如目标和方案数),使用
- 空间优化实现:
- 一维
dp
数组更新时,内层循环必须逆序(从大到小),保证每个物品只选一次。 - 初始化需单独处理
dp[0]
,如dp[0] = 1
(目标和问题)或dp[0] = 0
(最大价值问题)。
- 一维
算法思路核心
- 状态定义:
- 明确
dp[i][j]
的含义,例如“前i
个物品中选出体积不超过j
的最大价值”或“组成和为j
的方案数”。
- 明确
- 状态转移方程:
- 分“选”与“不选”两种情况讨论,注意合法性判断(如体积不足时不能选)。
- 累加或取最大值需根据问题目标调整。
- 边界条件处理:
- 初始化时,
dp[0][0]
通常有特殊含义(如空集和为0),其他位置根据问题设定初始值。 - 处理负值或非法状态(如
dp[j] = -1
表示无法凑出体积j
)。
- 初始化时,
- 问题转化技巧:
- 将复杂问题转化为背包模型,如“分割等和子集”转化为求子集和等于
sum/2
。 - 利用数学公式简化问题,如“目标和”中推导出
a = (sum + target)/2
。
- 将复杂问题转化为背包模型,如“分割等和子集”转化为求子集和等于