
01 背包问题是用来介绍动态规划算法最经典的例子。
一、解释 1
①、状态方程
f[i, j] = Max{ f[i-1, j-Wi] + Pi, f[i-1,j] } ( j >= Wi )
f[i,j] 表示在前 i 件物品中选择若干件放在承重为 j 的背包中,可以取得的最大价值。Pi 表示第 i 件物品的价值。
决策:为了背包中物品总价值最大化,第 i 件物品应该放入背包中吗 ?
②、实例
假设山洞里共有 a、b、c、d、e 等 5 件宝物,重量分别是 2、2、6、5、4,价值分别是 6、3、5、4、6,现在有一个承重为 10 的背包,怎么装背包,可以才能带走最多的财富。
只要你能通过找规律手工填写出上面这张表就算理解了 01 背包的动态规划算法。
首先要明确这张表是至底向上,从左到右生成的。
用 e2 单元格表示 e 行 2 列的单元格。它表示只有物品 e 时,有个承重为 2 的背包,那么这个背包的最大价值是 0,因为背包装不下。
对于 d2 单元格,表示只有物品 e、d 时,承重为 2 的背包,所能装入的最大价值,仍然是 0,因为物品 e、d 都不是这个背包能装的。
同理 c2 = 0,b2 = 3,a2 = 6。
对于承重为 8 的背包,a8 = 15 是怎么得出的呢?
根据 01 背包的状态转换方程,需要考察两个值:一个是 f[i-1, j](这里是 b8 = 9),另一个是 f[i-1, j-Wi] + Pi。
f[i-1, j] 表示有一个承重为 8 的背包,当只有物品 b、c、d、e 四件可选时,这个背包能装入的最大价值。
f[i-1, j-Wi] 表示有一个承重为 6 的背包(当前背包承重减去物品 a 的重量),当只有物品 b、c、d、e 四件可选时,这个背包能装入的最大价值。
Pi 指的是 a 物品的价值,即 6。
由于 f[i-1, j-Wi] + Pi = 9 + 6 = 15 大于 f[i-1, j] = 9,所以物品 a 应该放入承重为 8 的背包。
③、代码
物品信息类
@interface PackageItem : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, assign) NSInteger weight;
@property (nonatomic, assign) NSInteger value;
- (instancetype)initWithName:(NSString *)name weight:(NSInteger)weight value:(NSInteger)value;
@end
@implementation PackageItem
- (instancetype)initWithName:(NSString *)name weight:(NSInteger)weight value:(NSInteger)value
{
if (self = [super init]) {
self.name = name;
self.weight = weight;
self.value = value;
}
return self;
}
@end
非递归代码
{
NSArray * nameArr = @[ @"a", @"b", @"c", @"d", @"e" ];
NSArray * weightArr = @[ @(2), @(2), @(6), @(5), @(4) ];
NSArray * valueArr = @[ @(6), @(3), @(5), @(4), @(6) ];
NSMutableArray<PackageItem *> * bagItems = [NSMutableArray<PackageItem *> arrayWithCapacity:ARRAY_LENGTH];
for(int i = 0; i < nameArr.count; i++) {
PackageItem * item = [[PackageItem alloc] initWithName:nameArr[i]
weight:[weightArr[i] integerValue]
value:[valueArr[i] integerValue]];
bagItems[i] = item;
}
[self packageAlgorithm:bagItems bagSize:10];
}
/**
* @brief 01 背包算法
*/
- (void)packageAlgorithm:(NSArray *)bagItems bagSize:(NSInteger)bagSize
{
if (bagSize == 0 || bagItems.count == 0) return;
NSInteger bagMatrix[bagSize][bagItems.count]; // 是否选中数组
PackageItem * item;
NSInteger i = 0; // 背包容量,列数
NSInteger j = 0; // 物品数量,行数
// 初始化数组
for (; i <= bagSize; i++) {
for (j = 0; j < bagItems.count; j++)
bagMatrix[i][j] = 0;
}
// 打印二维数组内容
for (j = 0; j < bagItems.count; j++) {
for (i = 0; i <= bagSize; i++)
printf("%ld ", (long)bagMatrix[i][j]);
printf("
");
}
printf("
");
for (i = 0; i <= bagSize; i++) {
// 因为 item 数组是按照 a、b、c、d、e 的顺序排列的,所以这里需要倒着取
for (j = bagItems.count - 1; j > -1; j--) {
item = bagItems[j];
// 装不下
if (item.weight > i) {
// 价值总和为 0
if (j == bagItems.count - 1) {
bagMatrix[i][j] = 0;
}
// 价值总和为之前项的和
else {
// 因为 j 是倒着取的,所以这里 - 1 变成了 + 1
bagMatrix[i][j] = bagMatrix[i][j+1];
}
}
// 能装下
else {
// 是第一个物品,保存起来
if (j == bagItems.count - 1) {
bagMatrix[i][j] = item.value;
}
// 非第一个物品,求最大值
else {
// 因为 j 是倒着取的,所以这里 - 1 变成了 + 1
bagMatrix[i][j] = MAX(bagMatrix[i][j+1], bagMatrix[i - item.weight][j+1] + item.value);
}
}
}
}
// 打印二维数组内容
for (j = 0; j < bagItems.count; j++) {
for (i = 0; i <= bagSize; i++)
printf("%ld ", (long)bagMatrix[i][j]);
printf("
");
}
NSInteger curSize = bagSize; // 当前能装的空间
NSMutableArray * answer = [NSMutableArray arrayWithCapacity:bagItems.count];
// 以 a、b、c、d、e 的顺序获取
for (j = 0; j < bagItems.count; j++) {
item = bagItems[j];
// 剩下能装的空间为 0
if (curSize == 0) {
break;
}
// 根据变换公式从上至下获得物品
if (bagMatrix[curSize][j] - bagMatrix[curSize-item.weight][j+1] == item.value) {
[answer addObject:item.name];
curSize -= item.weight;
}
}
NSLog(@"%@", answer);
}
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 6 6 9 9 12 12 15 15 15
0 0 3 3 6 6 9 9 9 10 11
0 0 0 0 6 6 6 6 6 10 11
0 0 0 0 6 6 6 6 6 10 10
0 0 0 0 6 6 6 6 6 6 6
2019-01-25 22:12:03.795695+0800 Demo[1750:32295] (
a,
b,
e
)
二、解释 2
①、状态方程
max.F(n,C,x). x∈0,1
展开公式:
F(n,C,x) = x1∗v1 + x2∗v2 + ... + xn∗vn x1∗w1 + x2∗w2 + ... + xn∗wn ≤ C xi∈0,1
x 的取值范围为 0 或者 1,代表着这个物品选择拿或者不拿,最终找出这样的组合如:(1, 1, 1, 0, 0, 1)或(1, 1, 1, 0, 0, 1)使得 F(n,C,x) 最大。
我们假设一个函数 B(n,C) = max.F(n,c,x),也就是说 B 函数是一个能够自动组合 x 的取值使得 F(n,c,x) 达到最大。
再次理解这个 B(n,C) 这个函数的意义:从 n 个物品里面选取,容量为 C,能达到的最大价值。
如果想要在 n 个商品里选择,得到最大总价值,那么肯定得先在 n-1 个物品里面选择,得到最大价值后,然后考虑第 n 个物品要不要放进去?放进去会不会超过容量限制,会不会得到一个最大价值。我们就得到了一个函数。
B(n, C) = B(n−1, C); 没有多余的空间去放置最后一个物品
B(n, C) = max{ B(n−1, C), B(n−1, C−wn) + vn }; 如果有多余的空间去放置,则考虑是否要放置
B(n−1, C) 与 B(n−1, C−wn) 所对应的 F(n,c,x) 中的 x 的组合不一定相同,因为容量约束条件变了,一个是 C 一个是 C-wn。
②、实例
w = {1, 2}
v = {1, 2}
C = 2
解:B(2, 2) 为最大价值,如果我们拿最后物品 w = 2、v = 2,因为 w = 2 = C , 所以可以选择拿或者不拿。
拿:如果确定拿走最后一个物品,则 B(2, 2) = B(2-1, 2-2) + 2 = B(1, 0) + 2
不拿:如果确定不拿走最后一个物品,则 B(2, 2) = B(1, 2); 因为最后一个物品选择不拿,所以情景肯定变为从 1 个物品里面选,容量为 2,是否达到最大值,因此等式左右两边相等。
然后比较 B(1, 0) + 2 与B(1, 2) 哪个大,很明显,对于 B(1, 0) 已经没有容量去放置下一个物品,就相当于从 0 个物品里面选 B(1, 0) = B(0, 0) = 0, B(1, 0) + 2 = 2 则求解 B(1, 2) 代表着只能去选择第一件(w = 1, v = 1),不拿结果为 0,拿结果价值就为 1。
B(2, 2) = max{ B(1, 0) + 2, B(1, 2) } = max{ 2, 1 } = 2
③、递归代码
{
NSArray * nameArr = @[ @"a", @"b", @"c", @"d", @"e" ];
NSArray * weightArr = @[ @(2), @(2), @(6), @(5), @(4) ];
NSArray * valueArr = @[ @(6), @(3), @(5), @(4), @(6) ];
selectedArray = [NSMutableArray arrayWithCapacity:nameArr.count];
NSMutableArray<PackageItem *> * bagItems = [NSMutableArray<PackageItem *> arrayWithCapacity:nameArr.count];
for(NSInteger i = 0; i < nameArr.count; i++) {
PackageItem * item = [[PackageItem alloc] initWithName:nameArr[i]
weight:[weightArr[i] integerValue]
value:[valueArr[i] integerValue]];
bagItems[i] = item; // a、b、c、d、e
selectedArray[i] = @(0);
}
NSLog(@"%ld", (long)[self packageAlgorithm:bagItems bagNo:bagItems.count bagSize:10]);
[selectedArray enumerateObjectsUsingBlock:^(NSNumber * obj, NSUInteger idx, BOOL * stop) {
if (obj.integerValue) {
NSLog(@"%@ ", nameArr[idx]);
}
}];
}
/**
* @brief 01 背包算法。递归方式
*/
- (NSInteger)packageAlgorithm:(NSArray *)bagItems bagNo:(NSInteger)bagNo bagSize:(NSInteger)bagSize
{
if (bagItems.count == 0 || bagSize == 0 || bagNo == 0) return 0;
PackageItem * item = bagItems[bagNo - 1];
// 装不下
if (bagSize < item.weight) {
// 在剩余的物品中查找
return [self packageAlgorithm:bagItems bagNo:bagNo - 1 bagSize:bagSize];
}
else {
NSInteger more = [self packageAlgorithm:bagItems bagNo:bagNo - 1 bagSize:bagSize];
NSInteger less = [self packageAlgorithm:bagItems bagNo:bagNo - 1 bagSize:bagSize - item.weight] + item.value;
if (more < less) {
selectedArray[bagNo - 1] = @(1); // 拿
}
else {
selectedArray[bagNo - 1] = @(0); // 不拿
}
return MAX(more, less);
}
}
2019-01-26 00:11:13.897795+0800 Demo[3618:88346] 15
2019-01-26 00:11:13.897903+0800 Demo[3618:88346] a
2019-01-26 00:11:13.897979+0800 Demo[3618:88346] b
2019-01-26 00:11:13.898040+0800 Demo[3618:88346] e
三、DP 优于递归的好处
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划的基本思想大致是:若要解一个给定问题,需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增速时特别有用。