一、 简介
1. 背包问题是什么
背包问题是一个经典的组合优化问题,它可以被抽象为一个把物品放入背包中的过程,以求最终背包中物品价值的最大化。
2. 背包问题的分类
常见的背包问题主要分为以下三种:
- 01背包问题:每种物品最多只能装一次。
- 完全背包问题:每种物品可以无限次装入背包中。
- 多重背包问题:每种物品有限制次数可以装入背包中。
二、 0/1背包问题定义
1. 0/1背包问题的定义
0/1背包问题是一个经典的动态规划问题,指的是有n个物品和一个容量为W的背包,每个物品只能选择装入一次或不装入。在装入背包的物品总重量不能超过W的前提下,选择物品装入背包中,使得背包中物品的总价值最大。
2. 动态规划算法解决0/1背包问题
对于0/1背包问题,我们可以采用动态规划算法来解决。具体的解决思路如下:
- 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
- 状态转移方程:对于第i个物品,我们有两种选择,可以选择将其放入背包中,也可以选择不将其放入背包中。因此有如下状态转移方程:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]] + v[i])
其中w[i]是第i个物品的重量,v[i]是第i个物品的价值。
通过上述状态转移方程,我们可以求出前i个物品,容量为j时的最大价值。最后返回dp[n][W]即可。
3. 代码示例
下面是一个基于动态规划的0/1背包问题的代码实现
#include <vector>
#include <iostream>
using namespace std;
int knapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0)); // 定义状态
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= W; j++) {
if(j < w[i - 1]) {
// 当前物品不能装入背包
dp[i][j] = dp[i - 1][j];
} else {
// 当前物品可以装入背包
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]); // 状态转移方程
}
}
}
return dp[n][W]; // 返回前n个物品,容量为W时的最大价值
}
int main() {
vector<int> w = {
2, 3, 4}; // 物品重量
vector<int> v = {
4, 5, 6}; // 物品价值
int W = 5; // 背包容量
cout << "The maximum value is: " << knapsack(w, v, W) << endl; // 输出最大价值
return 0;
}
通过动态规划算法,该程序可以计算最大价值并输出,从而解决0/1背包问题。
三、 完全背包问题
1. 完全背包问题的定义
完全背包问题是一个经典的动态规划问题,在0/1背包问题的基础上进行了扩展,它指的是有n个物品和一个容量为W的背包,每个物品可以选择装入多次,装入物品的总重量不能超过背包容量W,目标是装入的物品总价值最大
2. 动态规划算法解决完全背包问题
对于完全背包问题,我们同样可以采用动态规划算法来解决。与0/1背包问题不同的是,在完全背包问题中,每一个物品可以选择装入多次,因此需要重新定义状态和转移方程。
- 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
- 状态转移方程:对于第i个物品,我们可以选择装入它或不装入它。如果选择装入它,则背包容量会减少w[i-1],同时总价值加上v[i-1]。因此有如下状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i-1]] + v[i-1])
其中w[i-1]是第i个物品的重量,v[i-1]是第i个物品的价值。
需要注意的是,对于完全背包问题,状态转移方程中的dp[i - 1][j - w[i - 1]]已经变为了dp[i][j - w[i - 1]],因为每个物品可以装入多次。
3. 代码示例
下面是一个基于动态规划的完全背包问题的代码实现
#include <vector>
#include <iostream>
using namespace std;
int knapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0)); // 定义状态
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= W; j++) {
if(j < w[i - 1]) {
// 当前物品不能装入背包
dp[i][j] = dp[i - 1][j];
} else {
// 当前物品可以装入背包
dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i - 1]] + v[i - 1]); // 状态转移方程
}
}
}
return dp[n][W]; // 返回前n个物品,容量为W时的最大价值
}
int main() {
vector<int> w = {
2, 3, 4}; // 物品重量
vector<int> v = {
4, 5, 6}; // 物品价值
int W = 5; // 背包容量
cout << "The maximum value is: " << knapsack(w, v, W) << endl; // 输出最大价值
return 0;
}
通过动态规划算法,该程序可以计算最大价值并输出,从而解决完全背包问题。
四、 多重背包问题
1. 多重背包问题的定义
多重背包问题同样是一个经典的动态规划问题,它指的是有n个物品和一个容量为W的背包,每个物品有对应的数量限制和重量以及价值,物品j最多能装k次,装入物品的总重量不能超过背包容量W,目标是装入的物品总价值最大。
2. 动态规划算法解决多重背包问题
与完全背包问题类似,多重背包问题也可以采用动态规划算法来解决。但是需要注意的是,与完全背包问题不同的是,多重背包问题中每个物品有着数量限制,所以需要重新定义状态和转移方程。
- 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
- 状态转移方程:对于第i个物品,我们可以选择装入它或不装入它。如果选择装入它,需要考虑它的数量限制k[i-1],背包容量会减少多个w[i-1],同时总价值加上多个v[i-1]。因此有如下状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-k[i-1]w[i-1]]+k[i-1]v[i-1], dp[i-1][j-2k[i-1]w[i-1]]+2k[i-1]v[i-1], ...)
其中k[i-1]是第i个物品的数量限制,w[i-1]是第i个物品的重量,v[i-1]是第i个物品的价值。
需要注意的是,转移方程中的数量部分需要列举每一种可能的数量情况,才能求出最大价值。
3. 代码示例
下面是一个基于动态规划的多重背包问题的代码实现
#include <vector>
#include <iostream>
using namespace std;
int knapsack(vector<int>& w, vector<int>& v, vector<int>& k, int W) {
int n = w.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0)); // 定义状态
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= W; j++) {
for(int t = 0; t <= k[i - 1] && t*w[i - 1] <= j; t++) {
// 列举每一种可能的数量情况
dp[i][j] = max(dp[i][j], dp[i - 1][j - t*w[i - 1]] + t*v[i - 1]); // 状态转移方程
}
}
}
return dp[n][W]; // 返回前n个物品,容量为W时的最大价值
}
int main() {
vector<int> w = {
2, 3, 4}; // 物品重量
vector<int> v = {
4, 5, 6}; // 物品价值
vector<int> k = {
2, 3, 1}; // 物品数量限制
int W = 10; // 背包容量
cout << "The maximum value is: " << knapsack(w, v, k, W) << endl; // 输出最大价值
return 0;
}
通过动态规划算法,该程序可以计算最大价值并输出,从而解决多重背包问题。
五、 混合背包问题
1. 混合背包问题的定义
混合背包问题是指有n个物品和容量为W的背包,每个物品有对应的价值和重量以及选择方式(0/1、多重、完全),要求在装入物品的总重量不超过背包容量的前提下,将背包的总价值最大化。
2. 动态规划算法解决混合背包问题
混合背包问题同时拥有0/1、多重和完全三种选择方式,而前两者的解法已经在前面的问题中讨论过。因此,我们只需考虑如何通过动态规划算法解决混合背包问题中的完全背包问题。
- 定义状态:用dp[i][j]表示前i个物品,容量为j时的最大价值。
- 状态转移方程:对于第i个物品,若采用完全背包的方式装入背包,则有如下状态转移方程:
dp[i][j] = max(dp[i][j], dp[i-1][j-kw[i-1]]+kv[i-1])
其中k为第i个物品的选择数量,w[i-1]为第i个物品的重量,v[i-1]为第i个物品的价值。
需要注意的是,完全背包问题中采用的是无限制的选取,因此我们只需在状态转移方程中将k从1到j/w[i-1]的所有情况都遍历一遍即可。
- 同样需要记得将前面两种选择方式的结果进行合并。
3. 代码示例
下面是一个基于动态规划的混合背包问题的代码实现
#include <vector>
#include <iostream>
using namespace std;
int knapsack(vector<int>& w, vector<int>& v, vector<int>& c, int W) {
int n = w.size();
vector<int> dp(W + 1, 0); // 定义状态,此处只需要开一个一维数组,因为混合背包只需要合并前两种选择方式的结果即可
for(int i = 1; i <= n; i++) {
if(c[i - 1] == 0) {
// 0/1选择方式
for(int j = W; j >= w[i - 1]; j--) {
dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
}
} else if(c[i - 1] == 1) {
// 多重选择方式
for(int j = w[i - 1]; j <= W; j++) {
dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
}
} else {
// 完全选择方式
for(int j = 1; j <= W; j++) {
for(int k = 1; k <= j / w[i - 1]; k++) {
// 遍历每一个可能的数量情况
dp[j] = max(dp[j], dp[j - k * w[i - 1]] + k * v[i - 1]);
}
}
}
}
return dp[W]; // 返回前n个物品,容量为W时的最大价值
}
int main() {
vector<int> w = {
2, 3, 4}; // 物品重量
vector<int> v = {
4, 5, 6}; // 物品价值
vector<int> c = {
0, 1, 2}; // 物品选择方式
int W = 10; // 背包容量
cout << "The maximum value is: " << knapsack(w, v, c, W) << endl; // 输出最大价值
return 0;
}
通过动态规划算法,该程序可以计算最大价值并输出,从而解决混合背包问题。
六、c++ 背包问题的优化
1. 背包问题的常见优化策略
在实际应用中,我们通常需要通过优化背包问题的算法以降低时间和空间复杂度,提高程序运行效率。以下是背包问题的常见优化策略:
- 优化状态转移方程:
在计算某个状态时,我们可以通过前面已经计算出来的状态来简化目标状态的计算。例如,我们可以将0/1背包问题的状态转移方程优化为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1])
即将原来的dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1], dp[i-2][j], dp[i-2][j-w[i-1]] + v[i-1]...)转换为只考虑前一行dp[i-1][ ]的值。
- 优化空间复杂度:
使用滚动数组可以将背包问题中的二维数组降为一维数组,从而只需要开O(W)的空间。
- 优化搜索顺序:
将物品按照单位重量价值从大到小排序,可以让后面的物品更有可能被选择,从而避免搜索过多无用的状态。
2. 代码示例
下面是一个基于优化策略的背包问题的代码实现
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
// 滚动数组版本的0/1背包问题
int knapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<int> dp(W + 1, 0);
for(int i = 1; i <= n; i++) {
for(int j = W; j >= w[i - 1]; j--) {
dp[j] = max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
}
}
return dp[W];
}
// 根据单位重量价值排序后的0/1背包问题
struct Item {
int w, v;
double unitValue; // 单位重量价值
};
bool cmp(Item a, Item b) {
return a.unitValue > b.unitValue; // 按照单位重量价值从大到小排序
}
int knapsack2(vector<Item>& items, int W) {
int n = items.size();
sort(items.begin(), items.end(), cmp);
vector<int> dp(W + 1, 0);
for(int i = 0; i < n; i++) {
for(int j = W; j >= items[i].w; j--) {
dp[j] = max(dp[j], dp[j - items[i].w] + items[i].v);
}
}
return dp[W];
}
int main() {
vector<int> w = {
2, 3, 4}; // 物品重量
vector<int> v = {
4, 5, 6}; // 物品价值
int W = 5; // 背包容量
cout << "The maximum value is: " << knapsack(w, v, W) << endl; // 输出最大价值
vector<Item> items = {
{
2, 4}, {
3, 5}, {
4, 6}};
cout << "The maximum value is: " << knapsack2(items, W) << endl; // 输出最大价值
return 0;
}
使用滚动数组优化后的0/1背包问题和通过排序优化后的0/1背包问题分别被实现,并输出了最大价值。
七、背包问题的应用
1. 背包问题在实际生活中的应用
背包问题是一个经典的组合优化问题,可以用于很多实际生活场景中,例如:
- 快递员在派送物品时,需要用最少的次数将所有物品全部送出去,即可把问题转化为背包问题。
- 旅行者要将一定数量的行李装在行李箱或背包里,而行李箱或背包有一个特定的容量,旅行者需要在有限的容量内带走价值最高的行李。
- 设计带宽优化问题,选择一定数量的数据包来传输,使得总价值最大,但不超过宽带承载能力。
2. 代码示例
下面是一个背包问题的代码示例
def knapsack(weights, values, capacity):
"""
使用动态规划思想解决背包问题
:param weights: 物品重量列表
:param values: 物品价值列表
:param capacity: 背包容量
:return: 背包装物品的最大价值
"""
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, capacity + 1):
if j < weights[i - 1]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
return dp[n][capacity]
if __name__ == '__main__':
weights = [2, 3, 4] # 物品重量
values = [4, 5, 6] # 物品价值
capacity = 5 # 背包容量
print("背包最大价值为:", knapsack(weights, values, capacity))
以上代码使用了动态规划的思想解决了背包问题,通过构建dp数组记录状态转移结果。在实现过程中,根据物品重量和背包容量的大小进行判断,得出最大价值。
八、 背包问题结语
1. 背包问题的意义和价值
背包问题是一类经典的组合优化问题,其在工业、经济、科学和技术等各个方面有着广泛的应用。一个更贴切生活的例子是,假设你有一个最大容量为 $V$ 的背包,有 $n$ 个物品,第 $i$ 个物品的重量为 $w_i$,价值为 $v_i$。现在你要从这 $n$ 个物品中选择一部分放入背包中,使得这些物品的总重量不超过 $V$,并且价值最大。这就是背包问题的典型描述。
背包问题的求解过程可以使用各种方法,如贪心、动态规划等。其中,动态规划是最常用的求解方法,常见的动态规划解法包括 0/1 背包问题、完全背包问题和多重背包问题等。
2. 学习建议
如果想要深入学习背包问题,可以从以下两个方面入手:
- 了解各种求解算法的思想与具体实现。背包问题的各个求解算法具有不同的适用范围和选材方式,对不同类型的问题适用的算法也不同。因此,了解并熟练掌握各种求解算法的思想和具体实现方法,可以更好地解决实际问题。
- 刷题练习。虽然背包问题的求解方法多种多样,但是题目的套路和解题思路却有一定的共性。不管是初学者还是进阶者,通过不断刷题和实践,学习更多的算法思想和解题技巧,对于学习和掌握背包问题都是非常有帮助的。
作为一名c++程序员,可以在实现背包问题的过程中,多注意以下几点:
- 注意使用STL库函数和语法糖,这可以提高程序开发效率。
- 注意代码的易读性和可维护性,可以采用面向对象的方式对代码进行重构。
- 注意C++语言的强类型和数组越界等问题。