背包
01 背包
0-1背包问题是一种非常经典的组合优化问题,广泛应用于经济学、管理科学、计算机科学等领域。在这个问题中,你有一个背包和一系列物品;每件物品都有自己的重量和价值。目标是确定哪些物品应该被选中并放入背包中,以使得背包内物品的总价值最大化,同时确保总重量不超过背包的承载能力。这个问题的名称来源于每件物品只能选择0个(不选)或1个(选中),不能选择部分物品或选择同一物品多次。
基本概念:
- 背包承载重量(Capacity):背包能够承载的最大重量。
- 物品(Items):每件物品都有两个属性,重量(Weight)和价值(Value)。
- 选择:对于每件物品,你可以选择放入背包(即选择该物品)或不放入背包(即不选择该物品)。
目标:
最大化背包中物品的总价值,同时确保选中物品的总重量不超过背包的承载能力。
正常解法
0-1背包问题可以通过动态规划(Dynamic Programming,DP)来解决。动态规划是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。
动态规划解法
- 状态定义:
dp[i][w]
表示考虑前i
件物品,在总重量不超过w
的条件下可以获得的最大价值。 - 状态转移方程:
- 如果不选择第
i
件物品,则dp[i][w] = dp[i-1][w]
。 - 如果选择第
i
件物品(前提是w
大于等于第i
件物品的重量),则dp[i][w] = dp[i-1][w-weight[i]] + value[i]
。 dp[i][w]
取上述两种情况的最大值。
- 如果不选择第
- 初始化:
dp[0][w] = 0
对于所有w
,表示没有物品时价值为0;dp[i][0] = 0
对于所有i
,表示背包容量为0时价值为0。 - 结果:
dp[n][W]
是问题的解,其中n
是物品数量,W
是背包的承载能力。
特点
- 优化:动态规划通过存储子问题的解来避免重复计算,从而达到优化计算过程的目的。
- 决策:每件物品的决策是独立的,即决策依赖于之前的选择,但不直接依赖于后来的选择。
代码解析:
#include <iostream> // 包含标准输入输出流库
#include <vector> // 包含向量容器库
using namespace std; // 使用标准命名空间,避免每次都要写std::
int main()
{
int maxw,n; // 声明整型变量maxw(背包最大重量)和n(物品数量)
cin>>maxw>>n; // 从标准输入读取maxw和n的值
vector<int> w(n+1),v(n+1); // 创建两个整型向量w和v,分别存储每件物品的重量和价值,大小为n+1(因为从1开始计数)
for(int i=1; i<=n; i++)
{
cin>>w[i]>>v[i]; // 循环读取每件物品的重量w[i]和价值v[i]
}
int maxn = -1; // 初始化最大价值maxn为-1,用于记录遍历过程中的最大价值
vector<vector<int>> dp(n+1,vector<int>(maxw+1,0)); // 创建一个二维向量dp,用于动态规划存储到达每个状态的最大价值
for(int i = 1; i<=n; i++) // 遍历每件物品
{
for(int j=1; j<=maxw; j++) // 遍历每种重量限制
{
if(j>=w[i])//如果当前重量限制j大于等于物品i的重量,即可以放入物品
{
// 决策:不放入当前物品,或放入当前物品,取二者的最大值
dp[i][j] = max(v[i] + dp[i-1][j-w[i]], dp[i-1][j]);
}
else // 如果当前重量限制小于物品i的重量,不能放入物品
{
dp[i][j] = dp[i-1][j]; // 不放入当前物品,价值等于不考虑当前物品时的价值
}
maxn = max(dp[i][j],maxn); // 更新最大价值
}
}
cout<<maxn; // 输出最大价值
return 0; // 程序正常结束
}
一维数组优化方法
一维数组优化是解决0-1背包问题的一种空间优化技术。在标准的动态规划解法中,我们通常使用一个二维数组dp[n+1][W+1]
来存储状态,其中n
是物品数量,W
是背包的最大重量。虽然这种方法很直观,但它需要较大的空间,特别是当物品数量和背包容量很大时。一维数组优化技术可以显著减少所需的空间复杂度,只使用一个一维数组dp[W+1]
来实现。
原理:
一维数组优化的基本原理是利用了这样一个事实:在计算dp[w]
的值时,只需要知道上一行dp
的值。因此,我们可以只用一个数组,逐个处理每个物品,更新这个数组。
实现步骤
-
初始化:初始化一个一维数组
dp[W+1]
,所有元素设置为0。dp[w]
表示当前背包容量为w
时,能够装入物品的最大价值。 -
状态转移:对于每个物品
i
,从背包的最大容量W
向下遍历到该物品的重量weight[i]
。对于每个容量w
,尝试将物品i
放入背包中,更新dp[w]
的值:- 如果放入物品
i
,则dp[w] = max(dp[w], dp[w-weight[i]] + value[i])
。
- 如果放入物品
-
遍历方向:注意,这里的遍历是逆向的(从大到小),原因是保证每个物品只被考虑一次。正向遍历可能会导致一件物品被重复计算,违反0-1背包问题的规则。
示例代码片段
vector<int> dp(W+1, 0);
for(int i = 0; i < n; ++i) {
for(int w = W; w >= weight[i]; --w) {
dp[w] = max(dp[w], dp[w-weight[i]] + value[i]);
}
}
优点
- 空间效率:一维数组优化方法将空间复杂度从
O(nW)
减少到O(W)
,其中W
是背包的最大重量。这对于内存使用是一个巨大的改进,尤其是在处理大规模问题时。
注意事项
- 逆向更新:必须逆向更新数组
dp[w]
,从W
向下到0
,以确保每个物品只计算一次。 - 适用性:这种优化只适用于0-1背包问题。对于完全背包问题(一个物品可以选多次),更新的方向应该是正向的。
一维数组优化技术是动态规划领域的一个重要优化,它展示了如何通过减少算法的空间复杂度来处理大规模问题,同时保持了问题解决的有效性和效率。
完整代码解析:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int maxw, n; // maxw 是背包最大容量,n 是物品数量
cin >> maxw >> n;
vector<int> dp(maxw + 1, 0); // 使用一维数组存储状态,初始化为0
for (int i = 0; i < n; ++i) {
int weight, value;
cin >> weight >> value; // 输入每个物品的重量和价值
// 逆序遍历背包容量,确保每个物品只被考虑一次
for (int w = maxw; w >= weight; --w) {
// 更新dp[w],决定是否将当前物品加入背包
dp[w] = max(dp[w], dp[w - weight] + value);
}
}
cout << dp[maxw]; // 输出最大价值,即dp数组最后一个元素
return 0;
}
完全背包
1780. 采灵芝
要解决这个问题,我们可以使用动态规划中的“完全背包问题”策略。这个问题的核心是在给定的时间限制内,找出能够采摘的灵芝组合,使得总价值最大化。每种灵芝可以被视为一个“物品”,每种灵芝的采摘时间可以视为其“重量”,每种灵芝的价值则直接对应其“价值”。与传统背包问题不同的是,每种灵芝可以采摘无限次,这是一个完全背包问题的特征。
解题思路
-
定义状态: 定义一个动态规划数组
dp
,其中dp[j]
表示在时间限制为j
时能够获得的最大价值。 -
状态转移方程: 对于每种灵芝,我们尝试将其加入到当前的采摘组合中,并更新
dp[j]
。具体来说,如果加入这种灵芝,我们需要检查所有可能的时间点,更新dp[j]
为max(dp[j], dp[j - time] + value)
,其中time
是采摘这种灵芝所需的时间,value
是这种灵芝的价值。 -
初始化:
dp[0]
初始化为0,表示在时间限制为0时,无法获得任何价值。 -
遍历所有灵芝: 对于输入中的每种灵芝,重复上述过程,更新动态规划数组
dp
。 -
结果:
dp[T]
即为在时间限制T
内可以获得的最大价值。
示例代码
基于上述思路,我们可以写出如下伪代码:
输入:总时间 T,灵芝种类数 M
初始化一个长度为 T+1 的数组 dp,所有元素值为 0
对于每种灵芝 i 从 1 到 M:
读入采摘这种灵芝需要的时间 time 和价值 value
对于每个时间点 j 从 time 到 T:
dp[j] = max(dp[j], dp[j - time] + value)
输出 dp[T]
这段思路的关键在于理解如何通过动态规划来迭代更新每个时间点可能达到的最大价值,以及如何通过重复选择某种灵芝来最大化总价值,而不仅仅是选择不同种类的灵芝组合。由于每种灵芝可以无限次采摘,我们需要在每个时间点考虑是否加入更多的同种灵芝,以提高总价值。
完整代码展示:
一维代码展示:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int T, M;
cin >> T >> M; // 读取时间限制和灵芝种类数
vector<int> dp(T + 1, 0); // 初始化动态规划数组
for (int i = 0; i < M; i++) {
int time, value;
cin >> time >> value; // 读取每种灵芝的采摘时间和价值
// 进行动态规划更新
for (int j = time; j <= T; j++) {
dp[j] = max(dp[j], dp[j - time] + value);
}
}
cout << dp[T] << endl; // 输出在规定时间内可以采到的灵芝的最大总价值
return 0;
}
二维代码展示:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int T, M;
cin >> T >> M;
vector<vector<int>> dp(M+1, vector<int>(T+1, 0));
for(int i = 1; i <= M; i++) {
int time, value;
cin >> time >> value;
for(int j = 0; j <= T; j++) {
dp[i][j] = dp[i-1][j]; // 不采摘这种灵芝
if(j >= time) {
dp[i][j] = max(dp[i][j], dp[i][j-time] + value); // 采摘这种灵芝
}
}
}
cout << dp[M][T] << endl; // 输出最大价值
return 0;
}