简介
介绍0-1背包问题的动态规划的解题思路及C++实现。
问题
已知存在N个宝物,每个宝物都有自己的质量m和价值v,在考虑选择宝物时只能选择总质量小于等于M的方案,请问在最 优方案下选择宝物,能获取到最大价值V是多少?
目前我们有一个背包,只有固定的容量,要解决的问题就是在一定容量的背包面前装哪几块宝石才能获取到最大的价值,对于每块宝物我们只有拿或者不拿这两种选择,拿为1不拿为0,因此叫做0-1背包问题
示例:
输入:
// 宝物的数量为5
N = 5
// 总质量不大于10
M = 10
// 每件宝物的质量
m = [2,2,6,5,4]
// 每件宝物的价值
v = [6,3,5,4,6]
输出:
// 能获取到最大价值V
15
// 能获取到最大价值时选取的宝物编号(第1个,第2个,第5个)
1,2,5
其他测试数据下载:
思路
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
以示例的数据来填以上表演示整个计算过程。
表格说明:
- 第A列表示物品的序号,B列表示物品的重量,C列表示物品的价值
- 以背包容量和物品数量来来建立价值数组
- 价值数组的填表过程是以第5个物品到第1个物品的方式填表,也就是说以尝试放入第5个物品,然后尝试放入第4个物品,。。。来填表
- 价值数组的值表示以K4为例,表示尝试到第2个物品,背包容量为7的情况下的最大价值9
计算演示:
-
初始化价值数组
d p [ N ] [ M + 1 ] = { 0 } dp[N][M+1]=\{0\} dp[N][M+1]={0} -
初始化最后一个物品5的价值数组。
-
当背包容量小于物品重量4,物品不能放入背包,所以D7-G7表格的价值为0
v [ N − 1 ] [ j ] = 0 ( N = 5 , 0 < j < 4 ) v[N-1][j] = 0 \\ (N=5,0<j<4) v[N−1][j]=0(N=5,0<j<4) -
当背包容量大于等于物品重量4,物品能放入背包,所以H7-N7表格的价值为6
v [ N − 1 ] [ j ] = 0 ( N = 5 , 4 ≤ j ≤ M ) v[N-1][j] = 0 \\ (N=5,4\leq j\leq M) v[N−1][j]=0(N=5,4≤j≤M)
-
-
计算从第4个物品到第1个物品尝试放入的情况的价值数组
以第4个物品为列,后续物品计算类似:
-
当背包容量小于物品重量5,物品不能放入背包,所以其价值应该为该背包容量情况下的第5个物品的价值。也就是D6-H6的内容等于D7-H7,其计算为:
v [ i ] [ j ] = v [ i + 1 ] [ j ] ( N = 4 , 0 < j < 5 ) v[i][j] = v[i+1][j]\\ (N=4,0<j<5) v[i][j]=v[i+1][j](N=4,0<j<5) -
当背包容量大于等于物品重量4,物品能放入背包,其分2种情况,取最大的价值的情况。以M6为例,
- 不放入物品时的,其价值为M7 = 6
- 放入物品时,那之前物品5的最大价值是I7=6(),然后再加上当前物品的价值4,其价值为10
- 取以上2种情况的最大值,那M6=10
其计算公式:
v [ i ] [ j ] = M a x ( v [ i + 1 ] [ j ] , v [ i + 1 ] [ j − m [ i ] ] + v [ i ] ) ( i = 3 , 5 ≤ j ≤ M ) v[i][j]=Max(v[i+1][j], v[i+1][j-m[i]]+v[i])\\ (i=3,5\leq j\leq M) v[i][j]=Max(v[i+1][j],v[i+1][j−m[i]]+v[i])(i=3,5≤j≤M)
-
实现
通过以上分析实现代码如下:
#include <vector>
#include <algorithm>
#include <iostream>
#include <iomanip>
// 物品结构体
struct Item
{
int weight;
int value;
};
void knapsack_1(int maxWeight, const std::vector<Item> &vecItem, int &retMaxValue, std::vector<int> &retSolutionItemIndex)
{
// 初始化价值数组
std::vector<std::vector<int>> itemValues(vecItem.size(), std::vector<int>(maxWeight + 1, 0));
// 初始化最后一个物品的价值数组
auto lastItemIndex = vecItem.size() - 1;
auto lastItem = vecItem[lastItemIndex];
for (size_t indexWeight = 0; indexWeight < maxWeight; indexWeight++)
{
if (lastItem.weight > indexWeight)
{
// 物品重量大于背包容量,不放入
itemValues[lastItemIndex][indexWeight] = 0;
}
else
{
// 物品重量小于背包容量,放入
itemValues[lastItemIndex][indexWeight] = lastItem.value;
}
}
// 计算从倒数第2个物品到第1个物品尝试放入的情况的价值数组
for (int itemIndex = lastItemIndex - 1; itemIndex >= 0; itemIndex--)
{
auto curItem = vecItem[itemIndex];
auto preItemIndex = itemIndex + 1;
for (int indexWeight = 0; indexWeight <= maxWeight; indexWeight++)
{
if (curItem.weight > indexWeight)
{
// 当前物品的重量大于背包的容量,放不进去,所以价值是上一个物品对应容量的价值
itemValues[itemIndex][indexWeight] = itemValues[preItemIndex][indexWeight];
}
else
{
// 当前物品的重量大于背包的容量,尝试放入,可能出现2种情况,取其中价值最大的:
// 不放入的价值更大:itemValues[preItemIndex][indexWeight]
// 放入的价值更大:itemValues[preItemIndex][indexWeight - curItem.weight] + curItem.value
// 备注:放入就是当前背包容量减去当前物品的重量,就是上一个物品尝试放入的背包容量;更加上一个物品尝试放入的背包容量的价值+当前物品的价值,就是该物品放入的价值。
itemValues[itemIndex][indexWeight] = std::max(itemValues[preItemIndex][indexWeight], itemValues[preItemIndex][indexWeight - curItem.weight] + curItem.value);
}
}
}
// 最优解:第一行的最后一个是最优解
retMaxValue = itemValues[0].back();
// 最优解的物品
auto curWeight = maxWeight;
for (size_t itemIndex = 0; itemIndex < lastItemIndex; itemIndex++)
{
auto curItem = vecItem[itemIndex];
// 如果当前物品的价值和下一个物品的价值不一致的,则当前物品被选中了;否则背包的容量不发生变化
if (itemValues[itemIndex][curWeight] != itemValues[itemIndex + 1][curWeight])
{
retSolutionItemIndex.push_back(itemIndex);
curWeight -= curItem.weight;
}
}
if (itemValues[lastItemIndex][curWeight] > 0)
{
retSolutionItemIndex.push_back(lastItemIndex);
}
}
int main()
{
int maxWeight = 10;
std::vector<Item> vecItem;
Item item;
item.weight = 2;
item.value = 6;
vecItem.push_back(item);
item.weight = 2;
item.value = 3;
vecItem.push_back(item);
item.weight = 6;
item.value = 5;
vecItem.push_back(item);
item.weight = 5;
item.value = 4;
vecItem.push_back(item);
item.weight = 4;
item.value = 6;
vecItem.push_back(item);
int maxValue = 0;
std::vector<int> solutionItemIndex;
knapsack_1(maxWeight, vecItem, maxValue, solutionItemIndex);
std::cout << "max value:" << maxValue << std::endl;
std::cout << "solutions:";
for (size_t item = 0; item < solutionItemIndex.size(); item++)
{
std::cout << (solutionItemIndex[item] + 1) << " ";
}
std::cout << std::endl;
return 0;
}
输出如下:
max value:15
solutions:1 2 5
优化
将价值数组降为到一维数组,代码试下如下:
注意:因将为一维数组后,只记录最优一个物品个背包容量情况的最优解,之前物品的背包容量的最优解无记录,所以不能获取到最优解选择的物品。
#include <vector>
#include <algorithm>
#include <iostream>
#include <iomanip>
// 物品结构体
struct Item
{
int weight;
int value;
};
void knapsack_2(int maxWeight, const std::vector<Item> &vecItem, int &retMaxValue, std::vector<int> &retSolutionItemIndex)
{
// 初始化价值数组
std::vector<int> dp(maxWeight + 1, 0);
// 计算
for (size_t indexItem = 0; indexItem < vecItem.size(); indexItem++)
{
auto curItem = vecItem[indexItem];
// 1.背包的容量小于物品容量的情况,不计算,其实就是上一次物品放入背包的情况
// 2 背包的容量大于当前物品的重量,尝试放入,可能出现2种情况,取其中价值最大的:
// 不放入的价值更大:dp[curWeight],也就是上一个物品在当前背包容量情况的最大价值
// 放入的价值更大:dp[curWeight - curItem.weight] + curItem.value,也就是去掉当前重量后,上一次背包的最大价值,然后加上当前物品的价值
// 3 为什么从背包的最大容量到当前物品的容量,而不能方向,因为dp[curWeight - curItem.weight]需要使用上一个物品在对应背包的价值,如果反向会覆盖
for (auto curWeight = maxWeight; curWeight >= curItem.weight; curWeight--)
{
auto preItemValue = dp[curWeight];
auto chooseItemValue = dp[curWeight - curItem.weight] + curItem.value;
if (chooseItemValue > preItemValue)
{
dp[curWeight] = chooseItemValue;
}
}
}
retMaxValue = dp.back();
}
int main()
{
int maxWeight = 10;
std::vector<Item> vecItem;
Item item;
item.weight = 2;
item.value = 6;
vecItem.push_back(item);
item.weight = 2;
item.value = 3;
vecItem.push_back(item);
item.weight = 6;
item.value = 5;
vecItem.push_back(item);
item.weight = 5;
item.value = 4;
vecItem.push_back(item);
item.weight = 4;
item.value = 6;
vecItem.push_back(item);
int maxValue = 0;
std::vector<int> solutionItemIndex;
knapsack_2(maxWeight, vecItem, maxValue, solutionItemIndex);
std::cout << "max value:" << maxValue << std::endl;
return 0;
}
完整测试工程打包下载地址: