龙空技术网

01背包问题解法 原创: Jiau 机器感知 8月7日

深圳风景2019 218

前言:

此刻朋友们对“01背包问题动态规划最优解”大体比较珍视,各位老铁们都需要了解一些“01背包问题动态规划最优解”的相关资讯。那么小编在网上汇集了一些对于“01背包问题动态规划最优解””的相关文章,希望咱们能喜欢,各位老铁们快快来了解一下吧!

01背包问题解法

原创: Jiau 机器感知 8月7日

未经许可,禁止转载

1. 定义

我们有$N$种物品,物品$i$的重量为$w[i]$,价格为$p[i]$。我们假定所有物品的重量和价格都是非负的,背包所能承受的最大重量W,如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题 。

2. 二维动态规划解法

二维动态规划其实就是填表,表格中的值表示能获得的最大总价值,表格如下:

图片来源于国外的一个网站,网站地址见附录,感兴趣的可以去看看。

现在我们分析动态规划中的状态转移,假设当前待求状态为table[i][j],其中,$i$表示第$i$个物品,$j$表示当前背包容量,由于是01背包问题,即一个物品只能选或者不选,也即当前最优解的可能有两种,即选与不选,对应的总价值分别为:table[i-1][j]、table[i-1][j-w[i]]+v[i],解释如下:

当不选择第$i$件商品时,当前最大价值就和状态$i-1$,容量为$j$时的最优解一样;当选择第$i$件物品时,当前最大价值就状态$i-1$,但容量为$j-w[i]$,再加上当前物品价值$v[i]$时的价值,由此可得,该问题的状态转移方程如下:

table[i][j] = max(table[i-1][j], table[i-1][j-w[i]]+v[i])

有了这个状态转移方程之后,我们就可以写程序了,示例程序如下:

// total weight

#define W (18)

// total stuff

#define N (7)

int table[N+1][W+1] = {0};

int value[N+1] = {0, 12, 10, 8, 11, 14, 7, 9};

int weight[N+1] = {0, 4, 6, 5, 7, 3, 1, 6};

void knapsack()

{

int k, w;

for (k=1; k<=N; k++) {

for (w=1; w<=W; w++) {

if (weight[k] > w) {

table[k][w] = table[k-1][w];

} else {

int value1 = table[k-1][w-weight[k]] + value[k];

int value2 = table[k-1][w];

if (value1 > value2) {

table[k][w] = value1;

} else {

table[k][w] = value2;

}

}

}

}

}

运行结果如下,对比网图可知,结果完全相同:

3. 最优解回溯

通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表格时的规则,反推可知:

a. 如果table[i][j] == table[i-1][j],这说明第$i$件商品没有被选择,则上一步就是在table[i-1][j]的位置处;

b. 如果table[i][j] != table[i-1][j],说明选择了第$i$件商品,再根据table[i][j] = table[i-1][j-w[i]] + v[i] 可得到上一个位置为table[i-1][j-w[i]]位置处;

c. 如此反复找到 i=0 为止。

示例代码如下:

void show_track()

{

vector<pair<int, int>> track;

int i=N, j=W;

while (i>0) {

if (table[i][j] == table[i-1][j]) {

i--;

} else {

track.push_back(make_pair(i, j));

int w = weight[i];

j -= w;

i--;

}

}

for (int i=0; i<track.size(); i++) {

cout << track[i].first << ' ';

}

}

4. 复杂度分析

根据程序分析可知,我们需要填的表格数量为$N*W$,因此时间复杂度和空间复杂度都是$O(N*W)$,因为要遍历完所有的可能性才可能得到最优解,而不像有序数组一样可以二分法或者什么的,所以时间复杂度没法优化了,但空间复杂度还是可以优化的,因为我们每次更新只与上一时刻状态有关,所以不需要保留这么多时刻的状态,空间复杂度可以优化到$O(W)$,首先分析上一步的核心代码:

for (w=1; w<=W; w++) {

for (k=1; k<=N; k++) {

if (weight[k] > w) {

table[k][w] = table[k-1][w];

} else {

int value1 = table[k-1][w-weight[k]] + value[k];

int value2 = table[k-1][w];

......

}

可以看出,核心代码部分,每次更新仅与上一时刻状态有关,对应到table中就是,每次更新只与table的一个行数据有关,这也就是空间复杂度为什么能够优化为$O(W)$的原因,如果简单的优化成如下代码:

for (w=1; w<=W; w++) {

for (k=1; k<=N; k++) {

if (weight[k] > w) {

table[w] = table[w];

} else {

// 需要 w 前方的 w-weight[k] 处的值

int value1 = table[w-weight[k] + value[k];

int value2 = table[w];

table[w] = ...;

}

上述代码不会得到正确的结果,为什么呢?行扫描我们是从左到右进行的,而我们需要取 $w-weight[k]$,即$w$左边的值,而我们却从左边开始更新的,那么就出现了我们需要的值被开始的更新覆盖了,得到的就是当前时刻的值了,不是期待的上一时刻的值,很显然不可能的得到正确的结果了,为了避免这种覆盖现象发生,我们可以通过反向扫描更新,即从右到左更新,这样更新的位置确定不会被左方的需要,就可以得到正确的结果了,代码如下:

void knapsack_optimization()

{

// 仅使用 table 中第一行

int k, w;

for (k=1; k<=N; k++) {

for (w=W; w>=1; w--) {

if (weight[k] > w) {

table[0][w] = table[0][w];

} else {

int value1 = table[0][w-weight[k]] + value[k];

int value2 = table[0][w];

if (value1 > value2) {

table[0][w] = value1;

} else {

table[0][w] = value2;

}

}

}

}

}

5. 总结

01背包问题解法使用的是二维动态规划进行求解的,朴素二维的动态规划时间复杂度和空间复杂度都是$O(N*W)$,而经过分析代码可以看出,状态转移时,当前时刻状态只与上一时刻的状态有关,所以空间复杂度可以优化到$O(W)$,在优化代码是需要注意的是:需要改变行更新顺序,为了避免新的状态覆盖上一时刻的状态,需要从右到左进行更新状态;有了最优解之后就是回溯最优解,方法步骤遵循上述步骤即可。

附录

标签: #01背包问题动态规划最优解