源码先锋

源码先锋

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

admin 62 128

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])

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

//totalweight

defineN(7)

inttable[N+1][W+1]={0};

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

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

voidknapsack()

{

intk,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{

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

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

if(value1value2){

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为止。

示例代码如下:

voidshow_track()

{

vectorpairint,inttrack;

inti=N,j=W;

while(i0){

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

i--;

}else{

_back(make_pair(i,j));

intw=weight[i];

j-=w;

i--;

}

}

for(inti=0;();i++){

couttrack[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{

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

intvalue2=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]处的值

intvalue1=table[w-weight[k]+value[k];

intvalue2=table[w];

table[w]=;

}

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

voidknapsack_optimization()

{

//仅使用table中第一行

intk,w;

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

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

if(weight[k]w){

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

}else{

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

intvalue2=table[0][w];

if(value1value2){

table[0][w]=value1;

}else{

table[0][w]=value2;

}

}

}

}

}

5.总结

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

附录