前言
背包0-1问题属于典型的求最大/最小子集问题范畴,它不像rod-cutting或matrix-chain-multiplication等问题,求解过程是按照单位等增或单位递减,0-1背包问题属于在集合范围内的某一个值,而且这些值大概率不是连续值。
问题描述
假定有N件物品,每件物品具有特定的价值value[i]和重量weight[i](1<=i<=N);现给定一个背包,此背包有总重量的限制W,求解这个包装载的物品具有最大价值和。
为什么把此问题称作0-1问题呢? 因为每件物品都有两种状态,如果没有选择,可以称作为状态0,如果选择,那么可以标记为状态1.
用具体的示例进行说明,为了阐述方便,我们规定物品有3件,背包最大承重W<=50kg, 表格勾画出问题。
Item # No. | Value | Weight |
---|---|---|
1 | 60 | 10 |
2 | 100 | 20 |
3 | 120 | 30 |
如果我们用穷举法,可以得到8类不同的组合,对于每个不同组合,可以计算其总价值和总重量,最后选择满足总重量要求的最大价值。
其中灰色代表左边的物件未放入背包,绿色代表物件已经背包,需要考虑其重量和价值。我们以C3选择为例说明,C3选择了物件1和物件2放入背包,那么我们就可以求出背包内所放置物品的价值总和 与 重量总和**(160,30)**。
s u m _ v a l u e = ∑ ( 60 + 100 ) = 160 sum\_value=∑(60+100)=160 sum_value=∑(60+100)=160
s u m _ w e i g h t = ∑ ( 10 + 20 ) = 30 sum\_weight=∑(10+20)=30 sum_weight=∑(10+20)=30
通过观察,可以发现C7包含的价值最大,为280;但同时如果考虑背包所能最大的承重 W<=50,那么显然C7不符合求解要求。再继续观察,我们发现 C6组合**(220,50)**既满足背包最大承重要求,也满足价值最大的要求,那么C6就是我们最终的选择,也即选择物件2和物件3放入背包,可以获得最大的价值。
针对三件物品,我们可以采用穷举法罗列所有可能的选项,如果物品件数较多,假设有10件物品,就需要罗列1024次才可能求出最终的解;假定有N件物品,如果采用穷举法,我们需要进行2^N 罗列才能求出解,显然这样效率很低,在N较大时候,程序运行效率很低,甚至无法求解。
按照《算法导论》的模板,仍然采用CRCC模式对此问题进行分析。
a) 表征最优解的结构(Characterize the structure of optimal solution)
要计算背包所能装载的最大价值,同时满足背包的承重要求。如果我们走一个极端,如果背包可以无限承重,那么此问题就转换为所有物品价值简单求和的问题,因为物品的价值总为正,物品越多,那么其放入背包后的价值就越大,在此极端情况下,我们不需要用到任何优化,运用动态规划意义不大。
所以我们最优解的结构一定包含两个参数,参数之一为物件的数量n(1<=n<=N),另外一个参为背包的最大承重W,作为约束条件物件的重量总和必须≤W.
如果采用朴素的函数语言,我们可以将表征最优解的结构抽象为函数结构:
f ( n , w ) = m a x { f ( n − 1 , w ) , f ( n − 1 , w − w e i g h t [ n ] ) + v a l u e [ n ] } o r f ( n − 1 , w ) f(n,w)=max\{f(n-1,w),f(n-1,w-weight[n])+value[n]\} or f(n-1,w) f(n,w)=max{f(n−1,w),f(n−1,w−weight[n])+value[n]}orf(n−1,w)
其中f(n-1,w)表示我们舍弃第n个物件,相当于直接跳过第n个物件,直接操作第n-1个物件,由于舍弃第n个物件,此选择的价值为0,也就是f(n-1,w)+0.
其中f(n-1,w-weight[n])+value[n](如果是C语言,要替换weight[n]\为weight[n-1],value[n]\为value[n-1])表示,把第n件物品的价值包含在内,然后往前回退,选择包含第n件物品后果:
当背包允许容纳第n件物品的时候,我们需要在放入(1)和不放入 (0)之间作出选择,取二者之间的最大值;
当背包不允许容纳第n件物品的时候,我们需要直接跳过第n件物品,采用前(n-1)件中的物品装入的价值;
值得一提的是, 函数f(n,w)中的n并非表示背包中装了n件物品,而是对n件物品完成放入/不放入选择后的评估后得到结果,背包的总装入件数<=n。
由于没件物品的重量是离散分布,函数f(n,w)中的w是离散量,并不是连续量,这和rod-cutting或matrix-chain-multiplication等问题连续分布不一样,如果我们设定w的范围(0= b)递归定义最优解的值(Recursively define the value of the optimal solution) 实际上上我们在上述分析中,已经完成了对最优解的值的定义, 在此省略。 c) 计算最优解的值(Compute the value of the optimal solution) 首先我们采用递归定义求解最优解的值,而且在递归过程中,我们先不采用memo记忆数组,这样做的目的是为了更好理解程序的执行流程。对于f(n,w)的递归终结条件为,当n=0的时候,也就是没有任何物件的时候,无论背包允许放入重量为多少,那么f(n,w)的值一定为0;如果背包允许放入的重量为0或为负数,那么f(n,w)的值也为0;这两种状态下递归需要返回的值为0。 假设我们现在有三件物品和一个背包,我们已经了解其基本信息如下: N=3, 物品件数 W=50, 背包所能承受的最大重量 value[N] = {60,100,120} weight[N] = {10, 20, 30} 通过递归,我们形成如下的递归树: 其中每个节点(n,weight),n代表目前已经对前n个物品做出选择,weight代表剩余重量(还可以容纳的重量,也即剩余容纳重量)。 其中黄色highlight部分未通过选择对背包增加的价值,价值0代表未把此物件放入背包;蓝色highlight部分为节点通过比较大小获取的结果(选择的结果),由于重量为离散值,而且当物件数目比较小的情况下,子树的重复性不算是非常明显,本例子中我们看到仅有(0,20)数值重复。 采用C语言,递归程序如下(无memo). P1. 定义头文件,knapsack_recursive.h P2. 函数实现,knapsack_recursive.c, 其中knapsack_resursive输入物件数量和背包总的容纳重量,进行top-down方式的递归实现。 P3. 主函数测试 对于递归函数的memo方法,请读者自行完成。 接下来让我们回到bottom-up的迭代模式,很多人在做动态规划的时候,喜欢迭代模式,因为迭代模式看起来优雅,并且代码实现很多时候比较简洁,但是bottom-up的挑战之一是定义dp数组,而且需要初始化某些前置的dp数组值,这两点都给bottom-up模式带来很大的挑战,就让我们来迎接挑战吧! 首先我们定义dp数组,我们有N件物品,可以把N件物品作为第一个维度;同时我们还有允许装入背包的总重量限制W;这两个值在过程中都是动态变化的,所以自然而然我们考虑到dp应该选取二维数组,dp[i][j]。 我们需要给dp[i][j]赋予bottom-up的含义,其含义为:对前面的i件物品做出选择判断,确保其总重不超过j的重量,j可以理解为还允许装入的重量。 dp数组的维度确认后,还需要确认dp数组之间的关联,用专业术语来讲,也就是所谓的状态方程的确认。 如果第i件物品允许装入背包,也即背包剩余的允许重量j>weight[i-1], weight[i-1]代表第i件物品的实际重量。在这种条件下,我们有两类选择: 选择装入,dp[i-1][j] 选择不装入,dp[i-1][j-weight[i-1]]+value[i-1] 此时dp[i][j]=max{dp[i-1][j], dp[i-1][j-weight[i-1]]+value[i-1]}。 开始遇到knapsack的时候,一直思考如何和j-1建立关联,实质上是走入了思维误区。除非有重量等于1的背包,否则j-1就是一类不存在的子问题,因为j代表背包剩余的允许重量,这时候就需要采用j-weight[i-1]为下标,代表装入第i件物品后,那么已经装入第i-1件的背包的最大允许重量减少至j-weigh[i-1],这个值也是i-1物件允许装入重量的上限。它相当于跨域多列,进行dp之间的关联。 由于背包的重量限制,第i件物品不允许装入背包,那么这时候的操作就比较简单,直接d[i][j]=dp[i-1][j]即可。 初始化dp[i][j]也相对而言比较简单,我们在i=0或j=0的条件下,赋值dp[0][j]=0或dp[i][0]=0,dp[0][j]表示没有物品,背包的允许重量无论多少,其转入物品的总价值都为0;dp[i][0]表示背包已经达到设计的重量,没有任何可以装入重量的余量(0表示允许剩余装入量)。 基于上面分析,我们来呈上bottom-up的代码。 对于bottom-up程序,我们可以对dp数组进行空间上的优化,可以采用dp[2][W+1]形式的数组,或者采用dp[W+1]数组,自右向左进行操作即可。 总结: 通过0-1背包问题的学习,更深刻认识到动态规划中0-1选择问题,以及由于重量的离散而导致与上一个问题关联的离散性。这个问题采用dp[i][j]更加直观和方便程序,当然也可以优化成dp[2][W+1]或dp[W+1]的数组进行迭代使用。 参考文献: 1.《Introduction to algorithm, 4ed》
/**
* @file knapsack_recursive.h
* @author your name ([email protected])
* @brief
* @version 0.1
* @date 2023-02-27
*
* @copyright Copyright (c) 2023
*
*/
#ifndef KNAPSACK_RECURSIVE_H
#define KNAPSACK_RECURSIVE_H
#include
/**
* @file knapsack_recursive.c
* @author your name ([email protected])
* @brief
* @version 0.1
* @date 2023-02-27
*
* @copyright Copyright (c) 2023
*
*/
#ifndef KNAPSACK_RECURSIVE_C
#define KNAPSACK_RECURSIVE_C
#include "knapsack_recursive.h"
int knapsack_resursive(int *weight, int *value, int n, int weight_capacity)
{
int incl_item;//include the current item
int excl_item;//exclude the current item
int max_value;
if(n==0 || weight_capacity==0)
{
return 0;
}
if (weight[n - 1] > weight_capacity)
// int value[N] = {60,100,120}; N=3; W=50
// int weight[N] = {10, 20, 30};
{
max_value = knapsack_resursive(weight, value, n - 1, weight_capacity);
}
else
{
excl_item=knapsack_resursive(weight,value,n-1,weight_capacity);
incl_item=knapsack_resursive(weight,value,n-1,weight_capacity-weight[n-1])+value[n-1];
max_value =max(excl_item,incl_item);
}
return max_value;
}
int max(int m, int n)
{
return (m>n?m:n);
}
#endif
/**
* @file knapsack_recursive_main.c
* @author your name ([email protected])
* @brief
* @version 0.1
* @date 2023-02-27
*
* @copyright Copyright (c) 2023
*
*/
#ifndef KNAPSACK_RECURSIVE_MAIN_C
#define KNAPSACK_RECURSIVE_MAIN_C
#include "knapsack_recursive.c"
int main(void)
{
int value[N] = {60,100,120};
int weight[N] = {10,20,30};
int n;
int w;
n=N;
w=W;
int max_value;
max_value=knapsack_resursive(weight,value,n,w);
printf("The maximum value is %d\n",max_value);
getchar();
return EXIT_SUCCESS;
}
#endif
/**
* @file knapsack_bottomup.c
* @author your name ([email protected])
* @brief
* @version 0.1
* @date 2023-02-27
*
* @copyright Copyright (c) 2023
*
*/
#ifndef KNAPSACK_BOTTOMUP_C
#define KNAPSACK_BOTTOMUP_C
#include "knapsack_bottomup.h"
void knapsack_bottomup(int *weight, int *value, int n, int capacity, int dp[N + 1][W + 1])
{
int i; //items indicator
int j; //weight indicator
int k;
//Initialize
//if there is no items inside, then it is worth zero value
for(k=0;k<=W;k++)
{
dp[0][k]=0;
}
// Initialize
// if the column contains zero weight, then it is worth zero value
for(k=0;k<=N;k++)
{
dp[k][0]=0;
}
for(i=1;i<=N;i++)
{
for(j=1;j<=W;j++)
{
if(j<weight[i-1]) //it can't fit the single weight of ith
{
dp[i][j]=dp[i-1][j];
}
else
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i-1]]+value[i-1]);
}
}
}
}
int max(int m, int n)
{
return (m>n?m:n);
}
#endif