问题描述

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

//总价值最高的可能的背包装载,x[i]只能等于0或者1
max{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,返回的价值为0
if(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的价值//装不下时返回0if(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的价值//装不下时返回0if(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;elsestd::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的重量是大于总容量的,则默认不装入物品iyMin = 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/1背包问题-----动态规划求解相关推荐

  1. 0/1背包问题——动态规划、回溯、分支限界法对比

    0/1背包问题--动态规划.回溯.分支限界法对比 2017.12.19 20:42:02 字数 3713 阅读 2820 目录 1.问题描述 1.1 问题描述 1.2 问题的数学表示(规划类问题,此种 ...

  2. 0/1背包问题——动态规划方法

    1.定义 动态规划:把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解. 2.求解步骤 (1)找到状态转化条件 (2)归纳状态转移方程 (3)定义初始条件值 3.实例解析--0/1背包 ...

  3. 01背包问题 动态规划求解方法 动态方程的详细解释 能理解的解释(附python代码)

    01背包问题属于组合优化问题:假设你要出门旅游,你现在有一个书包,这个书包的容量(capacity)有限,有很多物品如牙刷.防晒霜.雨伞.水杯等等,但书包装不下所有物品,因此我们必须有所取舍.那么通常 ...

  4. 01背包问题—动态规划求解

    动态规划 01 背包问题 关键代码 for (int i = 1; i <= n; ++i){for (int j = 0; j <= c; ++j){if (j < w[i]) / ...

  5. 0/1背包问题(蛮力法)

    问题描述: 给定n个重量为{w1,w2,w3,....,wn}.价值为{v1,v2,v3,...,vn}的物品和一个容量为C的背包,0/1背包问题是求解这些物品中的一个最有价值的子集,并且要能够装到背 ...

  6. 1008-----算法笔记----------0-1背包问题(动态规划求解)

    1.问题描述 给定n种物品和一个背包,物品i的重量是wi,其价值为vi,背包的容量为C.问:应该如何选择装入背包的物品,使得装入背包中物品的总价值最大? 2.问题分析 上述问题可以抽象为一个整数规划问 ...

  7. 01背包问题的动态规划求解及其C++实现

    本文讲解01背包问题的动态规划求解,并使用C++进行了实现 文章目录 01背包问题 动态规划 01背包问题的动态规划求解 01背包问题的动态规划求解-C++实现 01背包问题 有nnn个物品,这些物品 ...

  8. 0/1背包问题-----回溯法求解

    问题描述 有n个物品和一个容量为c的背包,从n个物品中选取装包的物品.物品i的重量为w[i],价值为p[i].一个可行的背包装载是指,装包的物品总重量不超过背包的重量.一个最佳背包装载是指,物品总价值 ...

  9. 【动态规划】0/1背包问题

    问题 H: [动态规划]0/1背包问题 时间限制: 1 Sec  内存限制: 64 MB 提交: 152  解决: 95 [提交] [状态] [讨论版] [命题人:admin] 题目描述 张琪曼和李旭 ...

最新文章

  1. SpringBoot------异步任务的使用
  2. YbtOJ#20067-[NOIP2020模拟赛B组Day5]糖果分配【dp】
  3. LintCode 550. 最常使用的K个单词II(自定义set(可修改数据的优先队列) + map)
  4. 产品经理的冬天来了嘛?
  5. 【OJ】洛谷排序题单题解锦集
  6. 【react】 使用react 脚手架 创建项目
  7. 计算机录屏幕和声音的软件是什么,哪个录屏软件可以录内部声音?分享开启与调节的方法...
  8. Ubuntu下SMPlayer播放器安装配置以及常用快捷键记录
  9. python常用的颜色英文表达_面料颜色中英文翻译对照表
  10. 【JAVA】五子棋2.0
  11. 位置不可用无法访问介质受写入保护怎样解决?
  12. git push时 please tell me who you are 或 git fatal: empty ident name (for <>) not llowed
  13. 计算机技术 在职,计算机技术在职研究生招生简章
  14. my read_university
  15. 安全生产施工单位材料准备清单
  16. 计算机系统基础崔丽群答案,2017届部分优秀教师风采展示——崔丽群
  17. 便捷节省的自动双面打印机或将成为趋势
  18. 织梦php的api,DedeCMS提交百度熊掌号API接口PHP提交
  19. JDOM/XPath解析XML简单示例
  20. 将Visio图片导入到Latex

热门文章

  1. 为什么下拉框拉不下来_为什么体重降不下来?4个饮食方法降低热量摄入,让体重降下来...
  2. Linux怎么确定信号来源,Linux信号来源和捕获处理以及signal函数简介
  3. 解决element-ui的表格设置固定栏后,边框线消失的bug
  4. Codeforces Beta Round #1 A,B,C
  5. 蓝桥杯历届试题 国王的烦恼(并查集逆序加边+坑)
  6. 在最美好的年华里,不要辜负最美的自己
  7. 迪杰斯特拉--- 模板(求最短路径/输出路径/所有路径都可以走的做法)
  8. python 正则表达式应用——缩写词扩充
  9. 本机连接虚拟机Oracle时报错的解决办法
  10. linux 环境搭建