0/1 背包问题题解
一、问题描述与分析
问题描述:一个旅行者外出旅行时需要将 n
件物品装入背包,背包的总容量为 m
。每个物品都有一个重量和一个价值。你需要根据这些物品的重量和价值,决定如何选择物品放入背包,使得在不超过总容量的情况下,背包中物品的总价值最大。
给定两个整数数组 weights
和 values
,其中 weights[i]
表示第 i
个物品的重量,values[i]
表示第 i
个物品的价值。你需要输出在满足背包总容量为 m
的情况下,背包中物品的最大总价值。
测试样例
样例1:
输入:
n = 3 ,weights = [2, 1, 3] ,values = [4, 2, 3] ,m = 3
输出:6
样例2:
输入:
n = 4 ,weights = [1, 2, 3, 2] ,values = [10, 20, 30, 40] ,m = 5
输出:70
样例3:
输入:
n = 2 ,weights = [1, 4] ,values = [5, 10] ,m = 4
输出:10
0/1 背包问题是一个经典的组合优化问题。给定一组物品,每个物品有其特定的重量和价值,以及一个容量为 m
的背包。我们需要在不超过背包容量的限制下,选择一些物品放入背包,目标是使放入背包的物品总价值最大,并且每个物品只能被选择一次。 例如,有 3 个物品,重量分别为 2
、1
、3
,价值分别为 4
、2
、3
,背包容量为 3
。我们需要找出一种选择物品的策略,使得放入背包的物品总价值最大。
二、数据结构选择
为了解决这个问题,我们采用一个二维数组 dp
,其中 dp[i][j]
表示在前 i
个物品中,容量为 j
的背包可以获得的最大价值。通过这样的数据结构,我们可以方便地记录在不同物品数量和背包容量下的最优解,以便后续进行状态转移和计算。
三、算法步骤
(一)初始化
- 当没有物品可选时(即
i = 0
),无论背包容量j
为多少,都无法放入物品,所以dp[0][j] = 0
。 - 当背包容量为 0 时(即
j = 0
),任何物品都放不下,所以dp[i][0] = 0
。
(二)状态转移
对于每个物品 i
(i
从 1 到 n
),以及每个背包容量 j
(j
从 1 到 m
),有两种决策:
- 不放入背包:此时
dp[i][j]
的值等于前i - 1
个物品在容量为j
的背包中的最大价值,即dp[i][j] = dp[i - 1][j]
。这是因为我们没有选择放入当前物品i
,所以背包中的物品情况与前i - 1
个物品时相同。 - 放入背包:如果当前背包容量
j
大于等于物品i
的重量weights[i - 1]
,那么我们可以考虑放入物品i
。放入物品i
后的价值为前i - 1
个物品在容量为j - weights[i - 1]
的背包中的最大价值加上物品i
的价值values[i - 1]
,即dp[i][j] = dp[i - 1][j - weights[i - 1]] + values[i - 1]
。这里需要注意的是,我们要取不放入和放入两种情况中的最大值,所以dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
。
(三)最终结果
经过上述的状态转移过程,dp[n][m]
就表示在给定的 n
个物品和容量为 m
的背包情况下,可以获得的最大价值。
四、代码解释
int solution(int n, std::vector<int> weights, std::vector<int> values, int m) {
// 创建一个二维数组 dp,dp[i][j] 表示前 i 个物品在容量为 j 的背包中能获得的最大价值
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, 0));
// 动态规划填表
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
// 不放入第 i 个物品
dp[i][j] = dp[i - 1][j];
// 如果当前容量 j 大于等于第 i 个物品的重量,考虑放入第 i 个物品
if (j >= weights[i - 1]) {
dp[i][j] = std::max(dp[i][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
}
}
}
// 返回最大价值
return dp[n][m];
}
在代码实现中:
- 首先创建了二维数组
dp
并初始化为 0,用于存储中间结果和最终答案。 - 然后通过两层循环进行动态规划填表。外层循环控制物品的数量,内层循环控制背包的容量。在循环内部,按照状态转移方程进行计算,不断更新
dp[i][j]
的值。 - 最后返回
dp[n][m]
作为最终的结果。
五、同类题目参考
(一)题目:完全平方数
- 题目描述:给定正整数
n
,找到若干个完全平方数(比如1, 4, 9, 16,...
)使得它们的和等于n
。你需要让组成和的完全平方数的个数最少。 - 思路:可以将每个完全平方数看作一个物品,其价值为 1(因为我们只关心个数),重量为该完全平方数的值。背包的容量就是给定的正整数
n
。 - 状态转移方程:设
dp[i]
表示组成和为i
的最少完全平方数个数,则dp[i] = min(dp[i], dp[i - j * j] + 1)
,其中j * j <= i
。这里是在尝试用不同的完全平方数j * j
去填充容量为i
的背包,取使用最少个数的情况。
(二)题目:零钱兑换
- 题目描述:给定不同面额的硬币
coins
和一个总金额amount
。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1
。 - 思路:把每种硬币看作一个物品,硬币的面额就是其重量,价值为 1(同样只关心个数)。背包容量为给定的总金额
amount
。 - 状态转移方程:设
dp[i]
表示凑成金额i
所需的最少硬币个数,则dp[i] = min(dp[i], dp[i - coins[j]] + 1)
,其中j
遍历所有的硬币种类且i - coins[j] >= 0
。即尝试用不同的硬币去填充容量为i
的背包,取使用最少硬币个数的情况。
通过对这些同类题目的分析,我们可以发现它们都具有类似的动态规划解题思路,都是将某种元素看作物品,根据其特定属性构建背包模型,然后通过状态转移方程逐步求解最优解。掌握 0/1 背包问题的解法有助于我们快速理解和解决这类相似的动态规划问题。