假设有一个背包,体积是 V,另外有 n 个物品,物品的体积分别是 v1, v2, ... vn,每个物品的价值是 w1, w1, ... wn。求怎么将物品放到背包里,才能使背包中物品的价值最大 ?
背包问题是一个典型的动态规划问题。动态规划问题中经常包含一个最字,比如最大价值 ? 最短路径 ? 动态规划问题的求解思路包括以下几点:
(1)只看眼前利益
动态规划,关键字是动态,也就是说结果是在变化的。在计算过程中,只看眼前利益,只要当前这种情况满足要求,那么这就是中间的一个结果。
所有情况都遍历完之后的眼前利益就是最终想要的结果。
下边的代码是找数组的最大值。FindMax() 函数中,找最大值的时候,max 一直是已经遍历的数据的最大值,一直在更新,体现了只看眼前利益;max 也一直在更新,直到把数据都遍历完,max 就是最终的结果。这就是动态规划。
cpp
#include <iostream>
int FindMax(int *data, int size) {
int max = -1;
for (int i = 0; i < 4; i++) {
if (data[i] > max) {
max = data[i];
}
}
return max;
}
int main()
{
int data[4] = {100, 200, 50, 10};
std::cout << "max: " << FindMax(data, 4) << std::endl;
return 0;
}
(2)选与不选
选与不选,就是分类讨论的思想。比如背包问题,当考虑一个物品时,要考虑两种情况,即这个物品放入背包的话,最终能放入的最大价值是多少;这个物品不放入背包的话,最终能放入的价值最大是多少。如果有 n 个物品,每个物品都要做这样的分类讨论,共有 2 的 n 次方种组合。把这些所有的情况的价值都计算出来,哪个组合的价值最大,那么这个组合就是最终的结果。
(3)记录历史信息
在动态规划的计算过程中,对一些情况的讨论往往会重复,在计算过程中可以记录历史信息,那么可以减小后边的重复计算。
背包问题分为两类:0-1 背包和无限背包。0-1 背包,说的是每个物品的数量只有一个, 也就是这个物品要么放进去,要么不放进去,只有两种情况。无限背包说的是每个物品的数量有无数个,每个物品都可以放 0 个,1 个或者多个。
数据遍历是很多算法的基础。
无论是排序算法,还是搜索算法或者动态规划。
算法的基础就是对一定数量的数据进行遍历,在遍历的过程中嵌入自己的算法逻辑,逻辑的不同就产生了了不同的算法。
数据保存在数据结构中,比如数组,链表,二叉树,图。每种数据结构都有自己的遍历方法。
1 0-1 背包
牛客网 01背包链接。
使用一维数组,时间复杂度是 O(n);使用选与不选的原始算法,时间复杂度是 O(2 的 n 次方)。所以优先选用以为数组。
1.1 一维数组
cpp
#include<iostream>
#include<vector>
using namespace std;
// 能够放下的最大价值
int MaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
// 数组的长度是 V + 1
// 之所以比背包的体积大 1,这样就最大可以使用 V 做数组的下标了,便于使用
std::vector<int> value(V + 1);
// 数组元素的值是这个体积下能放下的价值,初始化为 0
value.assign(V + 1, 0);
// 两层循环,第一层循环遍历物品
for (int i = 0; i < n; i++) {
// 第二层循环遍历背包剩余的空间,能放下当前这个物品的空间
// 体积从大到小进行遍历
for (int j = V; j >= v[i]; j--) {
// value[j] 是没放这个物品的时候,背包在 j 这个体积下的价值
// value[j - v[i]] + w[i] 是放下这个物品的时候,j 体积下的价值
if (value[j - v[i]] + w[i] > value[j]) {
value[j] = value[j - v[i]] + w[i];
}
}
}
return value[V];
}
// 背包正好装满时的最大价值
int FullMaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
std::vector<int> value(V + 1);
// 与背包能放下的最大值比较的话
// 初始值是不一样的
// 将 value[0] 初始化为 0, 其它的元素初始化为一个非常小的数
// 这个非常小的数要保证物品价值都加起来和这个数相加,也不会大于 0
// 这样能保证正好装满的时候,value[V] 是大于 0 的
// 最后可以通过 value[V] 是不是大于 0 来判断背包是不是可以正好装满
// 如果能正好装满,那么 value[V] 价值是在 value[0] 也就是 0 的基础上加上物品的价值
// 所以,value[V] 是大于 0 的。
// 如果不能正好装满,那么 value[V] 的价值,在计算过程中,肯定与一个非常小的数进行了相加
// 所以 value[V] 是小于 0 的
value.assign(V + 1, -99999999);
value[0] = 0;
for (int i = 0; i < n; i++) {
int tmp_v = v[i];
int tmp_w = w[i];
for (int j = V; j >= tmp_v; j--) {
int value_old = value[j];
int value_new = value[j - tmp_v] + tmp_w;
if (value_new > value_old) {
value[j] = value_new;
}
}
}
if (value[V] < 0) {
return 0;
}
return value[V];
}
int main()
{
int n = 0;
int V = 0;
std::vector<int> v;
std::vector<int> w;
cin >> n >> V;
for (int i = 0; i < n; i++) {
int a = 0;
int b = 0;
cin >> a >> b;
v.push_back(a);
w.push_back(b);
}
std::cout << MaxValue(v, w, n, V) << std::endl;
std::cout << FullMaxValue(v, w, n, V);
return 0;
}
1.2 选与不选
选与不选使用递归算法。递归算法的时间复杂度是O(2的 n 次方),时间复杂度太大,在牛客网上运行经常超时。使用一维数组的方式,时间复杂度是 O(n),所以有限选择数组的方式。
cpp
#include<iostream>
#include<vector>
using namespace std;
// 保存背包能放得下的最大价值
int max_value = 0;
// 保存背包正好放满时的最大价值
int max_full_value = 0;
// 已放入的物品的价值
int value = 0;
// 已放入的物品的体积
int volume = 0;
void MaxValue(std::vector<int>& v, std::vector<int>& w, int n, int V,
int index) {
if (volume > V) {
return;
}
if (index == n) {
if (volume <= V && value > max_value) {
max_value = value;
}
if (volume == V && value > max_full_value) {
max_full_value = value;
}
return;
}
for (int i = index; i < n; i++) {
// 选择这个物品
volume += v[i];
value += w[i];
MaxValue(v, w, n, V, i + 1);
// 不选择这个物品
volume -= v[i];
value -= w[i];
MaxValue(v, w, n, V, i + 1);
}
}
int main() {
int n = 0;
int V = 0;
std::vector<int> v;
std::vector<int> w;
cin >> n >> V;
for (int i = 0; i < n; i++) {
int a = 0;
int b = 0;
cin >> a >> b;
v.push_back(a);
w.push_back(b);
}
MaxValue(v, w, n, V, 0);
std::cout << max_value << std::endl;
std::cout << max_full_value;
return 0;
}
2 无限背包
无限背包也叫完全背包,牛客网链接如下。
无限背包,说的是每个物品的个数都有无限个。可以使用一维数组的方式来求解,与 01 背包不同的是,在遍历体积的时候,需要从小到大进行遍历,01 背包是从大到小进行遍历的。
为什么从小到大进行遍历呢,这样对于一个物品可以遍历到放置个的情况。比如一个背包的体积是 10,一个物品的体积是 2。如果从小到大进行遍历,那么只放这个物品的话,可以放置 5 个这样的物品,体积遍历到 2 的时候,可以放一个,4 的时候可以再放一个,以此类推。如果从大到小进行遍历,从 10 遍历到 2,那么只能放置一个,不能在前边放置的基础之上,再次进行放置。
cpp
#include <iostream>
#include <vector>
using namespace std;
int MaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
std::vector<int> value(V + 1);
value.assign(V + 1, 0);
for (int i = 0; i < n; i++) {
int tmp_v = v[i];
int tmp_w = w[i];
for (int j = tmp_v; j <= V; j++) {
int value_old = value[j];
int value_new = value[j - tmp_v] + tmp_w;
if (value_new > value_old) {
value[j] = value_new;
}
}
}
return value[V];
}
int BagFullMaxValue(std::vector<int> &v, std::vector<int> &w, int n, int V) {
std::vector<int> value(V + 1);
value.assign(V + 1, -99999999);
value[0] = 0;
for (int i = 0; i < n; i++) {
int tmp_v = v[i];
int tmp_w = w[i];
for (int j = tmp_v; j <= V; j++) {
int value_old = value[j];
int value_new = value[j - tmp_v] + tmp_w;
if (value_new > value_old) {
value[j] = value_new;
}
}
}
if (value[V] < 0) {
return 0;
}
return value[V];
}
int main() {
int n = 0;
int V = 0;
std::vector<int> v;
std::vector<int> w;
cin >> n >> V;
for (int i = 0; i < n; i++) {
int a = 0;
int b = 0;
cin >> a >> b;
v.push_back(a);
w.push_back(b);
}
int max_value = MaxValue(v, w, n, V);
int bag_full_max_value = BagFullMaxValue(v, w, n, V);
std::cout << max_value << std::endl;
std::cout << bag_full_max_value << std::endl;
}