实验目的:
通过01背包问题理解动态规划,并根据题意描述写出自己的动态转移方程,并且练习将动态转移方程转换成程序进行实现。
实验环境:
win7,VS 2010
实验测试数据:
实验代码:
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std ;
/**
here we set there are 1...n objects
and the bag can contain the objects with the total
weight no more than m
each object 's weight is stored in int w[1..n]
w[i] is the i-th object's weight ,(1<= i <= n)
symmertically ,p[i] (1<=i<=n)is used to store
the price of the i-th object.
bool x[1..n] , this variable is used to store the final value
in the pattern of the solution vector ,
which is usually used in liner algebra calculation .
for instance , when we choose the no. 1, 4, 7 objects into the knapsack ,
we can obtain the global maximum value under the condition
w[1] + w[4]+ w[7] <= m (the total weight the knapsack can contain)
then the value in x can be presented as
x = [ 1 , 0 , 0 , 1 , 0 , 0 , 1, 0,0 ,0...0 ]
object no. 1 2 3 4 5 6 7 ...
int optp[0...n][0...m]
the 2-dimension to store the possible values
we define the optp[i][j] ( 0<=i<=n && 0<= j <= m )is the maximum value ,
which we got from the 1...i-the objects', under the condition that :
total weight of the i objects is less than m that the maximum weight
the bag can bear.
optp[n+1][m+1] is used to store the final maximum value ,
*/
#define n 5
#define m 100
void input (int w [], int p[])
{
int i ;
cout<<"input weight"<<endl ;
for ( i = 1 ; i <= n ; i++ )
{
printf("w[%d] = \n", i ) ;
scanf("%d" , &w[i]) ;
}
cout<<"input price"<<endl ;
for ( i = 1 ; i <= n; i++ )
{
printf("p[%d] = \n", (i));
scanf("%d" ,&p[i]) ;
}
}
int myKnapsack_DP ( int w[] , int p[] ,bool x[] )
{
int (*optp)[m+1] = new int [n+1][m+1] ;
int i , j , v = 0 ;
/*
first we initialize the values in optp [0][0..m]
which means that maximum value we get from by puting
0 objects into the 0..m weight-bearing knapsack is none .
*/
for ( i= 0 ; i <=m ; i++ )
optp[0][i] = 0 ;
/**
this means that , if we put [0..n] objects into the 0 weight-bearing
knapsack , we get the maximum value is still none .
and then we initialize the solution vectors x[0..n-1]
which means we choose none object at the begining .
*/
for ( i = 0 ; i <= n ; i++ )
{
optp[i][0] = 0 ;
x[i] = false ;
}
for ( i = 1 ; i <= n ; i++ )
{
for ( j = 1 ; j <= m ; j++ )
{
optp[i][j] = optp[i-1][j] ;
// printf("current optp[%d][%d] = %d\n" , i , j , optp[i][j]) ;
if ( j >= w[i] && (optp[i-1][j-w[i]]+p[i]) > optp[i-1][j] )
{
// printf("swaping \n current optp[%d][%d] = %d \n" , i , j , optp[i][j]) ;
// printf("current optp[%d][%d] = %d \n", (i-1), (j-w[i]) , optp[i-1][j-w[i]]) ;
optp[i][j] = optp[i-1][j-w[i]] +p[i] ;
}
}
}
j = m ;
for ( i = n ; i > 0 ; i-- )
{
if (optp[i][j] > optp[i-1][j])
{
x[i] = true ;
j = j- w[i] ;
}
}
v = optp[n][m] ;
delete optp ;
return v ;
}
int main ( int argc , char *argv [] )
{
int w[n+1], p[n+1] ;
bool x [n+1] ;
input ( w, p) ;
int result = myKnapsack_DP( w, p , x) ;
cout<<"here is what we choose"<<endl ;
for (int i = 1 ; i <=n ; i++ )
{
if (x[i])
{
cout<<"weight "<<w[i];
cout<<" price"<<p[i]<<endl ;
}
}
cout<<endl ;
cout<<"maximum value is "<<result <<endl ;
system("pause") ;
return 0 ;
}
实验思想:
1.状态转移方程
状态转移方程是动态规划方法的核心所在,而状态转移方程的编写往往和题目中的约束条件和目标函数二者是息息相关的。
对于01背包问题,用数学建模的方法可以这样来理解:
首先确定约束条件,我们希望从 n 个物品中选取其中的一个或是多个, 是指满足重量之和 <= 背包所能够承载的最大重量。
其次是目标函数,在满足约束条件的前提下,使得背包中的物品的价值达到最大值。
约束条件可以表示为如下的数学表达式:
而与之相对应的,目标函数可以表示成如下的数学表达式:
2.通过动态规划求取到全局最优解之后,确定解向量的方法
而我们所需要做的就是找到一组解向量, X = {x1, x2, ... , xn } 来使得解向量满足上述的数学关系表达式。
首先,是初始化程序中的相关变量。
即,根据已知条件可知,向背包承重为0 的背包中装入 0...n 个物体所最终得到的最大价值为 0 。
同样,向背包承重为 0...m 的背包中装入 0 个物体所得到的最大价值也是 0 。
按照这种想法我们就有了上述的第一个等式。
然后,是状态转移,设当前带检测的物品是 i 物品,并同时设背包在装入前面的(i-1) 个物品之后载重量为 j 。
我们知道背包的载重量是随着物品的不断的加入而变小的,只是不同的选择对应不同的解向量 x ,同时对应着当前的背包的不同的载重量。
如果 i 物品的重量 w[i] > j , 则第i个物品是无法装入到背包中的,因为背包的载重量不足以容纳物品 i 了,所以在这个状态下,装入前 i-1 个物品
所得到的最大价值与装入 i 个物品所取得的最大价值是相同的。
ps:在这里请注意的是,装入i-1 个物品也好装入 i 个物品也好,只是说明当前你的解向量所访问的维数,
我在这里说装入 i 个物品说明的是 |x| = i , 而第 i 个物品究竟是否是需要装入到背包中 即 xi = 0 还是 xi = 1 ,这个是由
物品的重量和当前背包可容纳的物品重量两个条件来共同决定的。
基于这种想法,我们可以得到上述的状态转移方程表达式中的第一个。
接下来,需要分析的情况是 w[i] <= j , 的情况,这种情况对应的是当前背包的载重量是可以容纳第 i 个物品的,但是这并不是说,
可以容纳就一定要将第 i 个物品装入值背包中的,这个时候取决于目标函数所计算出的数值,
如果计算出把第 i 个物品放入背包中所得到的最终价值最大,那么就放入。
如果计算出把第i 个物品不放入背包中,而选取其他的物品组合放入背包中所得到的最终价值最大,那么就不放入。
在这里应该知道的是,第 i 个物品选择放入与不选择放入这两种情况,所对应的前面的 i-1 个物品的选取情况也是不同的。
情况1 :选择放入第 i 个物品这种情况决定了,在前面的 i-1 个物品的选取是在背包承重量为 j- w[i]
(即当前重量排除选择放入的第 i 个物品的重量) ; 这种情况下对应的最大价值。
情况2 :不放入第 i 个物品,这种情况决定了,在前面的 i-1 个物品中选取的是在背包承重量为 j (因为选择不放入 i ,
所以承重量不变)这种情况下所选取求取最大值的解向量。
而情况1 , 所对应的当前价值就是:在前i-1 个物品基于载重量为 j - w[i] 的情况下所选取的最大价值 + 第 i 个物品的价值 p[i]
而情况2 , 所对应的是,不选取 i 物品,这个与第一个式子所标示的含义是一样的
即 optp[i][j] = optp[i-1][j] , 即选取前 i-1 个物品所对应的最大价值 = 选取前 i 个物品的最大价值,
因为第 i 个物品没有选,状态没有变遇上一个状态对应的数值是一致的。
=======================================
以上的情况是状态转移方程用于求解全局最优解的描述,等到循环结束之后,我们所需要求解的最优解,
会被存放于optp[n][m] 中。
接下来将要介绍的是,如何在求取最优解之后,通过最优解的值来反推,得到这个最优解的解向量 x 是怎样的组合。
如果 optp[i][j] > optp[i-1][j] 这种情况对应的是, 在背包的载重量为 j 的情况下,
选取前 i 个物品所对应的最大价值 > 选取前 i-1 个物品所对应的物品的最大价值,则必定有物品 i 被选入背包中。
如果 optp[i][j] <= optp [i-1][j] 这种情况对应的是,在背包的载重量为 j 的情况下,
选取前面 i 个物品所对应的最大价值 <= 选取前 i -1 个物品所对应的最大价值,则物品 i 必定没有被选入到背包中。
上述的两个过程其实就是上述状态转移方程的递向推导的过程。
于是我们就有了下面的伪代码:
j <- m
for i = n -> 1
if (optp[i][j] > optp[i-1][j])
x[i] <- true ;
j <- j - w[i]
前提是,在一开始的时候,将 bool x [ 1..n ] 中的数值全部初始化为 false
也就是说,在确定 i 被装入到背包中之后,不仅要将 x [i] 数值更新为 true ,
而且,还需要同步更新一下当前背包的载重量,使其相应减少物品 i 的重量 w[i] ,
然后在进行后续的判断。
实验注意事项:
在代码编写的时候,存放数据的数组下标是一件很考验人耐心的地方,
首先,optp[][] 二维数组中的下标访问范围分别是[0..n+1] [0..m+1]
其中 optp[0][0..m] 中所存放的数据是将 0 个物体放入值承重为 [0..m+1] 的背包中,所获取的最大的价值。
而, optp[0..n][0] 中所存放的数据是将 0..n+1 个物体放入值承重为 0 的背包中,所获取的最大的价值。
当然这两种情况所获取的最大价值都是 0 。
optp[n][m] 在最终循环结束的时候,存放的是背包问题所求解的全局最大值,也就是全局最优解。
为了配合 optp[0..n][0..m] 的访问区间,必须要将 p (用于存放物品价值的数组) 和 w (用于存放物品重量的一位数组)
还有存放解向量的 x 一位数组。
的相应访问区间从 [0..n-1] , => 存放 n 个元素 ,修改成 [1..n] => 同样也是存放 n 个元素。
实验改进:
可以将 w,p 写入到一个struct 中,这样操作起来也会变得很方便。
对于最后逆向推导的地方,是可以使用递归方法来对它进行改写的,这个地方的处理很符合递归的思想。