0/1背包问题-----动态规划求解

来源:互联网 发布:淘宝开放平台人工客服 编辑:程序博客网 时间:2024/06/09 06:37

问题描述

有n个物品和一个容量为c的背包,从n个物品中选取装包的物品。物品i的重量为w[i],价值为p[i]。一个可行的背包装载是指,装包的物品总重量不超过背包的重量。一个最佳背包装载是指,物品总价值最高的可行的背包装载。
我们要求出x[i]的值。x[i] == 1表示物品i装入背包,x[i] == 0表示物品i没有装入背包。
问题的公式描述是:

//总价值最高的可能的背包装载,x[i]只能等于0或者1max{p[1] * x[1] + p[2] * x[2] + ... + p[i] * x[i] + ... + p[n] * x[n]}

约束条件

//装入的所有物品的重量之和不大于背包容量size_t totalWeight = 0;for(size_t i = 1; i <= n; ++i){    totalWeight += w[i] * x[i]; //x[i] = 0 || x[i] = 1}if(totalWeight <= c)    //约束条件成立else    //约束条件不成立

问题分析

考察这样一个0/1背包问题: 总共4个物品,重量分别是w[1:4] = {8, 6, 2, 3},价值分别是p[1:4] = {8, 6, 4, 5},规定背包容量为12(即可以容纳的最大重量为12),求出获得最大价值的解情况。
如前所述,在该问题中,我们需要选择x[1],…,x[n]的值。假定我们按物品i = 1,2,…,n的顺序选择x[i]的值。如果选择x[1]=0,那么背包问题就转变为物品2,3,…,n,背包容量仍为c的问题。如果选择x[1]=1,那么背包问题就转变为物品2,3,…,n,背包容量为c-w[1]的问题。
所以在确定了物品i是否装入之后,背包问题就转变为i+1,i+2,…,n这些物品装入背包容量为c’或c’-w[i]的子问题(c’表示确定物品i是否装入之前背包所剩的容量)
又因为物品i装入与不装入是等可能的,在确定物品i的时候我们无法确定是装入它可以获得最优解还是不装入它可以获得最优解,所以需要分别计算这两种情况,然后取最优的一个。

假设f(i, y)表示容量为y,物品为i,i+1,…,n的背包问题的最优解的值,也就是说f(i, y)返回的值是物品从i到n,容量为y这个子背包可以获得的最大价值(最优解的值)。所以可以写出f的函数表达式:

//对于f(n, y)而言,即考虑最后一个物品是否装入时,//如果剩余容量大于物品n的重量(即足够装入物品n),则返回的就是物品n的价值,//否则,不装入物品n,返回的价值为0if(y >= w[n])    f(n, y) = p[n];else //y >= 0 && y < w[n]    f(n, y) = 0;
//对于f(i, y)而言,i >= 1 && i < n,即考察除最后一个物品之外的其他物品是否装入时,//如果当前可用容量小于物品i的重量(即装不下物品i),则表示x[i] = 0,//返回当前容量仍为y,物品从i+1到n的背包子问题的最优解//如果当前可用容量大于等于物品i的重量(即可以装下物品i),则物品i装入和不装入是等可能的,//需要考虑两种情况取最优的那一个if(y >= w[i])    f(i, y) = max(f(i+1, y), f(i+1, y-w[i]) + p[i]);else //y >= 0 && y < w[i]    f(i, y) = f(i+1, y);

根据上述分析得到的公式结论,即最优序列由最优子序列构成的结论,可以利用上述f的递归式实现递归求解

递归求解

很多类似的题都是利用递归的方式求解的,因为递归的思路比较简单,容易理解。唯一的缺点就是递归会造成大量的栈空间消耗(因为每层递归都会传参,又要保留上层递归程序运行的地址,同时又存在返回值的传递)。不过抛开这些,了解递归是如何求解问题还是很有必要的,后面会有利用迭代的方法求解。

为了减少递归调用不必要的传参消耗,可以把很多变量作为全局变量,在背包问题中,可以把背包容量Capacity,物品数量n,每个物品的重量(显然用数组存储比较合理)Weight[],以及每个物品的价值(同样利用数组存储)Profit[]作为全局变量。
这样递归函数只需要两个参数就可以了(像上面的f(i, y)一样),i表示物品i,y表示当前剩余容量。然后一步步将上面的公式转换成代码。

//全局变量int n;int *Weight;int *Profit;//函数返回的是物品i,i+1,...,n,背包容量为y时的最优解int Backpack(size_t i, size_t y){    //考察f(n, y)的情况    //可以装下物品n时返回物品n的价值    //装不下时返回0    if(i == n)    {        return y >= Weight[i] ? Profit[i] : 0;    }    //考察f(i, y)的情况    //分两种情况,取最大值    //第一种:物品i没有装入背包,可用容量y不变,开始装入物品i+1,...,n    //第二种:物品i装入背包,可用容量变为y-Weight[i],开始装入物品i+1,...,n.同时还应加上物品i的价值    else    {        return max(Backpack(i + 1, y),                   Backpack(i + 1, y - Weight[i]) + Profit[i]);    }}

之后考虑一个问题,0/1背包问题求得的结果是每个物品的装入情况,换句话说就是求x[i](i >= 1 && i<=n),当x[i] == 0时表示物品i没有被装入背包,当x[i] == 1时表示物品i被装入背包。

上面的递归程序可以让我们求得f(i, y),即当前可用容量为y,解决物品i,i+1,…,n的背包子问题。返回的值是这个子问题可以获得的最大价值量,也就是最优解。

然后考虑一下f(i, y)和f(i+1, y)的含义(这里两个y表示的数值相同,都表示考虑物品i的装入问题时,当前背包的可用容量)
f(i, y)表示从物品i到物品n可以获得的总价值(不知道i是否被装入)
f(i+1,y)表示的是物品i没有装入背包的情况下,从物品i到物品n可以获得的总价值。
所以只有当物品i没有被装入背包时,f(i, y)才与f(i+1, y)相等

综上,判断物品i是否被装入背包,只需要判断f(i, y)是否等于f(i+1, y)即可。所以在递归过程中还需要记录每一个f(i, y)的值,用于在最后输出装入情况。

f(i, y)的值可以记录在二维数组中,把它作为全局变量使用。更新后的代码如下:

//全局变量int n;int *Weight;int *Profit;int **Solution; //Solution[i][y]表示当前背包容量为y,物品i,i+1,...,n的背包装入情况可以获得的最优解的值//初始时Solution[i][y] = 0//函数返回的是物品i,i+1,...,n,背包容量为y时的最优解int Backpack(size_t i, size_t y){    //当Solution[i][y]不为0时,说明在之前已经计算过这种情况下的背包子问题,可以直接返回    //这样做可以减少重复的递归计算    if(Solution[i][y] != 0)        return Solution[i][y];    int solution;    //考察f(n, y)的情况    //可以装下物品n时返回物品n的价值    //装不下时返回0    if(i == n)    {        solution = y >= Weight[i] ? Profit[i] : 0;    }    //考察f(i, y)的情况    //分两种情况,取最大值    //第一种:物品i没有装入背包,可用容量y不变,开始装入物品i+1,...,n    //第二种:物品i装入背包,可用容量变为y-Weight[i],开始装入物品i+1,...,n.同时还应加上物品i的价值    else    {        solution = max(Backpack(i + 1, y),                       Backpack(i + 1, y - Weight[i]) + Profit[i]);    }    //存储求得的最优解    Solution[i][y] = solution;    return Solution[i][y];}

到目前为止递归程序就大致完成了。整个递归下来每种情况的最优解都记录在Solution二维数组中,而获得的最大价值则是递归程序Backpack(1, Capacity)的返回值,可以根据Solution二维数组输出背包装入的结果。

void printSolution(){    //最大价值量是Backpack(1, Capacity)的返回值    size_t pCurrentCapacity = Capacity;    for(size_t i = 1; i < n; ++i)    {        //如果f(i, y) == f(i+1, y)说明物品i没有被装入背包        if(Solution[i][pCurrentCapacity] == Solution[i+1][pCurrentCapacity])        {            std::cout << "Backpack" << i << ": " << 0 << std::endl;        }        //反之,则被装入背包,相应的剩余背包容量要减去物品i的重量        else        {                       std::cout << "Backpack" << i << ": " << 1 << std::endl;            pCurrentCapacity -= Weight[i];        }    }    //判断最后一个物品时不能像判断前n-1个物品一样,因为最后一个物品没有第n+1个物品    //所以只需要判断剩余容量能否装入物品n即可    if(pCurrentCapacity >= Weight[n])        std::cout << "Backpack" << n << ": " << 1 << std::endl;    else        std::cout << "Backpack" << n << ": " << 0 << std::endl;}

迭代求解

相比递归,迭代求解的好处是减少了递归反复调用的栈开销(这也是为什么将多数变量作为全局变量而不作为参数传递的原因),但是迭代在理解上并没有递归那样易于理解。

递归是从想要求的解开始(这里是Backpack(1, Capacity)),一步步深入到最底层,比如最开始只是调用Backpack(1, Capacity),一层层递归到Backpack(n, y),然后逐层向上返回。
而迭代则恰好与递归相反,也可以理解为迭代是将递归向上返回的过程呈现出来。也就是从物品n开始,最后求得物品1,得到想要的结果。

考虑两个事情。
1.递归程序中使用的二维数组Solution,Solution[i][y]表示当前可用容量为y,物品i,i+1,…,n的背包装入情况的最优解的值。
2.f(i, y)的公式中分为两种情况,一种是y小于物品i的重量,此时f(i, y) = f(i+1, y)。另一种是y大于等于物品i的重量,此时f(i, y)等于装入物品i和不装入物品i这两种情况的最大值

结合二者来看
1.当Solution[i][y]中的y小于物品i的重量时,Solution[i][y]应该等于Solution[i+1][y]的值。
2.当Solution[i][y]中的y大于等于物品i的重量时,Solution[i][y]应该等于Solution[i+1][y]和Solution[i+1][y-Weight[i]] + Profit[i]中的最大值。

又因为在程序执行过程中,y的值可能是从0到Capacity中的任何一个,所以需要把每种情况都计算在内。分割线便是物品i的重量:
1.y从0到Weight[i]-1是一种情况,此时当前可用容量不足以装入物品i,Solution[i][y] = Solution[i+1][y]。
2.y从Weight[i]到Capacity是另一种情况,此时当前可用容量可以装入物品i,Solution[i][y] = max(Solution[i+1][y], Solution[i+1][y-Weight[i]] + Profit[i])。

程序中需要把二维数组Solution中的每一个元素都计算出来。

void Backpack(int Weight[], int Profit, int n, int Capacity){    int **Solution = new int*[n];    for(size_t i = 1; i <= n; ++i)    {        Solution[i] = new int[Capacity+1];        for(size_t j = 0; j <= Capacity; ++j)            Solution[i][j] = 0;    }    //单独考虑最后一个物品,    //y在0到Weight[n]-1时,表示当前可用容量装不下物品n,f(n, y) = 0    //y在Weight[n]到Capacity时,表示当前可用容量可以装下物品n,f(n, y) = Profit[n]    int yMin = min(Weight[n] - 1, Capacity);    for(size_t y = 0; y <= yMin; ++y)        Solution[n][y] = 0;    for(size_t y = Weight[n]; y <= Capacity; ++y)        Solution[n][y] = Profit[n];    //考虑从物品n-1到2    //y在0到Weight[i]-1时,表示当前可用容量装不下物品i,f(i, y) = f(i+1, y);    //y在Weight[i]到Capacity时,表示当前可用容量可以装下物品i,f(i, y) = max(f(i+1, y), f(i+1, y - Weight[i]) + Profit[i]);    for(size_t i = n - 1; i > 1; --i)    {        //取总容量和物品i重量的较小值        //因为当物品i的重量是大于总容量的,则默认不装入物品i        yMin = min(Weight[i] - 1, Capacity);         for(size_t y = 0; y <= yMin; ++y)            Solution[i][y] = Solution[i+1][y];        for(size_t y = Weight[i]; y <= Capacity; ++y)            Solution[i][y] = max(Solution[i+1][y],                                  Solution[i+1][y-Weight[i]] + Profit[i]);    }    //单独考虑物品1,它不需要求出y从0到Capacity的所有情况,只需求得Solution[1][Capacity]即可    Solution[1][Capacity] = Solution[2][Capacity];    if(Capacity >= Weight[1])        Solution[1][Capacity] = max(Solution[1][Capacity],                                    Solution[2][Capacity-Weight[1]] + Profit[1]);}

在递归程序中,考虑完第i个物品后,开始考虑第i+1个物品时,剩余容量是间断的几个值,即要不是y,就是y-Weight[i],其他的值不需要考虑。然而在迭代程序中,程序是从第n个物品开始考虑的,然后考虑n-1,n-2,…,1,又因为当前剩余容量的定义是考虑完第1,2,…,i-1个物品后,考虑第i个物品时可用的容量。这就导致了无法预先知道在考虑完第i个物品后,考虑第i-1个物品时的剩余容量是多少,这就需要把所有可能的剩余容量都考虑一遍。
输出每一个背包是否装入的情况,和递归程序的输出函数是一样的,因为Solution中的一些关键部分的值都已经求好了。

0 0
原创粉丝点击