难度参考
难度:困难
分类:动态规划
难度与分类由我所参与的培训课程提供,但需 要注意的是,难度与分类仅供参考。且所在课程未提供测试平台,故实现代码主要为自行测试的那种,以下内容均为个人笔记,旨在督促自己认真学习。
题目
动态规划经典问题01背包?
具体内容:
背包最大重量为4
物品如下:
重量 | 价值 | |
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的最大重量是多少?
思路
0-1 背包问题的动态规划解法基于以下思路:
-
子问题定义:
- 将原问题分解为子问题。在这里,我们考虑"对于给定的背包容量和一组物品,最大价值是多少?"
- 定义状态
dp[i][w]
—— 表示考虑前i
个物品时,对于不超过w
重量的背包,所能得到的最大价值。
-
初始条件:
- 对于
dp[0][...]
,即一个物品也不考虑的情况,背包的价值总是 0。 - 对于
dp[...][0]
,即背包容量为 0 的情况,背包的价值也总是 0。
- 对于
-
递推关系(状态转移方程):
- 对于每一个物品
i
和每一个可能的重量w
,我们需要做出一个决策:是否将物品i
放入背包。 - 如果不放物品
i
,则dp[i][w] = dp[i-1][w]
—— 也就是说,当前的最大价值与前i-1
个物品时的最大价值相同。 - 如果放物品
i
,我们必须确保当前背包的剩余容量至少是物品i
的重量 (weights[i-1] <= w
),在这种情况下,dp[i][w] = dp[i-1][w-weights[i-1]] + values[i-1]
—— 也就是说,当前的最大价值是物品i
的价值加上剩余重量在前i-1
个物品中能得到的最大价值。
- 对于每一个物品
-
填表:
- 我们按照递推关系来填表 —— 从较小的
i
和w
开始,直到i
等于物品个数,w
等于背包最大重量。
- 我们按照递推关系来填表 —— 从较小的
-
解的构造:
- 表格填完后,
dp[n][maxWeight]
就是答案 —— 即考虑所有物品和最大背包重量时的最大价值。
- 表格填完后,
补充-递推公式的推导:
假设我们有n
个物品,每个物品的重量为weights[i]
,价值为values[i]
,背包的最大重量限制为maxWeight
。我们定义dp[i][w]
为考虑前i
个物品(物品编号为0
到i-1
),在背包重量限制为w
的情况下,能够得到的最大总价值。我们想求的最终答案是dp[n][maxWeight]
。
对于每个物品i
(从1
开始计数,对应数组下标为i-1
),以及每个可能的重量限制w
,我们面临两种选择:
-
不包含当前物品:如果我们决定不包含当前考虑的物品
i-1
,那么最大价值不会因为当前物品的存在而改变,所以它等于没有考虑当前物品时的最大价值,即dp[i-1][w]
。 -
包含当前物品:如果我们决定包含当前考虑的物品
i-1
,前提是这个物品的重量不超过当前的重量限制w
(即weights[i-1] <= w
)。此时,我们需要加上这个物品的价值values[i-1]
,同时,背包的剩余重量变为w - weights[i-1]
,因此我们还需要加上在新的重量限制下,考虑前i-1
个物品时的最大价值,即dp[i-1][w-weights[i-1]] + values[i-1]
。
因此,递推公式如下:
- 如果
weights[i-1] > w
(当前物品重量超过背包限制),则dp[i][w] = dp[i-1][w]
。 - 否则,
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
。
这个递推公式基于以下逻辑:对于每个物品,我们都做出是否包含它的决定,以确保在不超过背包重量限制的情况下达到最大价值。这种方式确保了我们能够考虑所有可能的组合,并找到最优解。
示例
初始状态
首先,初始化一个表`dp`,其中`dp[i][j]`代表在只考虑前`i`个物品,且背包容量为`j`时的最大价值。初始化时,所有值均为0。
物品情况:
- 物品0:价值15,重量1
- 物品1:价值20,重量3
- 物品2:价值30,重量4
动态规划表格是这样的(行表示物品,列表示重量):
重量 0 1 2 3 4
不选物品 0 0 0 0 0
加入物品0
加入物品0(价值15,重量1)后,我们更新表格。对于重量1及以上的列,我们可以加入物品0。
重量 0 1 2 3 4
不选物品 0 0 0 0 0
加入物品0 0 15 15 15 15
加入物品1
接着,我们考虑加入物品1(价值20,重量3)。我们更新表格,考虑对于每个重量限制,在不超过该重量的情况下,是否加入物品1能获得更高的价值。
重量 0 1 2 3 4
不选物品 0 0 0 0 0
加入物品0 0 15 15 15 15
加入物品1 0 15 15 20 20
在重量为4时,我们比较加入物品1后的价值与之前的状态。但是,我们忽略了一种可能:加入物品1(重量3,价值20)后,还可以再加入物品0(重量1,价值15),因此,对于重量4,最大价值应该是35(物品0和物品1的组合)。
加入物品2
最后,我们考虑加入物品2(价值30,重量4)。同样,我们更新表格,考虑对每个重量限制,加入物品2是否能带来更高的价值。
重量 0 1 2 3 4
不选物品 0 0 0 0 0
加入物品0 0 15 15 15 15
加入物品1 0 15 15 20 35
加入物品2 0 15 15 20 35
在重量为4的情况下,加入物品2的价值(30)并不比已有的组合(物品0和物品1,总价值35)更优,因此我们保持原有的最大价值不变。
最终结果
最终,我们得到的动态规划表如下,表明在背包重量限制为4时,我们能获得的最大价值是35,通过组合物品0(价值15,重量1)和物品1(价值20,重量3)。
重量 0 1 2 3 4
不选物品 0 0 0 0 0
加入物品0 0 15 15 15 15
加入物品1 0 15 15 20 35
加入物品2 0 15 15 20 35
因此,最终答案是,在确保总重量不超过4的条件下,我们最后得到的背包中的最大价值是35。
梳理
这个方法能够实现的原因在于它利用了动态规划(Dynamic Programming, DP)的两个关键性质:最优子结构和重叠子问题。
最优子结构意味着问题的最优解包含着其子问题的最优解。对于0-1背包问题来说,每增加一个物品的选择,都是基于之前所有物品选择的最优解上进行的新增决定。换句话说,在考虑是否加入当前物品时,我们可以依赖于之前物品的决策结果,这些决策结果是以最大化价值为目标的。
重叠子问题指的是在解决问题的过程中,相同的问题会被多次解决。在0-1背包问题中,当我们分别计算每一个重量限制下的最大价值时,会重复计算某些重量限制下的最大价值。通过动态规划,我们可以将这些价值存储在一个表中,避免重复计算,这就是为什么我们使用一个二维数组来存储每个子问题的答案。
在填充动态规划表时,我们从最小的子问题开始,即背包没有任何物品时的价值是0。然后逐步增加物品和重量,直到考虑了所有物品和所有重量限制。在每一步,针对每一个重量限制,我们都计算了两种情况:
1. 不加入当前的物品,直接使用之前同样重量限制下的最大价值。
2. 尝试加入当前的物品,将物品的价值加上剩余重量限制下的最大价值。
通过比较上述两种情况的价值,我们可以选择较大的一个作为当前重量限制和当前物品下的最大价值。这个过程一直持续到表格被填满,最终我们在表格的右下角得到的值就是我们能够拿到的最大价值。
简单来说,这方法之所以行之有效,是因为它将一个复杂问题分解为一系列叠加的简单问题并解决它们,同时保留了要求的全局最优解。通过表格的逐步填充,我们保证了在任何给定重量限制下,我们都是在做出最佳决策,以此计算出全局最优解。
代码
#include <iostream> // 引入标准输入输出库
#include <vector> // 引入向量(动态数组)库
using namespace std; // 使用标准命名空间,简化代码// 动态规划解决 0-1 背包问题的函数
int knapsack(const vector<int>& weights, const vector<int>& values, int maxWeight) {int n = weights.size(); // 物品数量// 初始化动态规划表,所有值起始为0vector<vector<int>> dp(n + 1, vector<int>(maxWeight + 1, 0)); // 使用动态规划构建 dp 数组for (int i = 1; i <= n; ++i) { // 遍历每个物品for (int w = 1; w <= maxWeight; ++w) { // 遍历每种重量限制// 如果当前物品可以加入背包if (weights[i - 1] <= w) {// 选择加入当前物品和不加入当前物品中价值更大的一个dp[i][w] = max(dp[i - 1][w], values[i - 1] + dp[i - 1][w - weights[i - 1]]);} else {// 如果不能加入当前物品,保持之前的最大价值dp[i][w] = dp[i - 1][w];}}}// 返回最大值,即考虑所有物品且不超过最大重量限制时的最大价值return dp[n][maxWeight];
}int main() {int maxWeight = 4; // 背包的最大重量限制vector<int> weights = {1, 3, 4}; // 物品的重量vector<int> values = {15, 20, 30}; // 物品的价值// 输出背包能达到的最大总价值cout << "背包能达到的最大总价值为 "<< knapsack(weights, values, maxWeight) << endl;return 0; // 程序正常结束
}
时间复杂度:O(n * maxWeight)
空间复杂度:O(n * maxWeight)