当前位置:   article > 正文

动态规划-(0-1)背包问题_0-1背包问题

0-1背包问题

目录

证明最优子结构

​编辑

图解

递归关系

代码规整版(书上的代码就是函数前面加了book的)

老师那个vs6老六编译器,不支持变量定义数组大小版本

图解​编辑

图解(查找是否获取图解即traceback函数)


下面我用容积代表重量(我是不会承认我看走眼了的。又懒得改。)

0-1背包问题是动态规划背包问题系列的最基础的一个问题。

相对理解起来较为简单。

按书上来说,要证明一个问题是否可以使用动态规划思想,需要满足最优子结构的性质,那么什么是最优子结构的性质呢?

书上给出的定义如下图

 看起来很拗口,其实就是不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。说白了就是一个最优策略的子策略也是必须是最优的,而所有子问题的局部最优解将导致整个问题的全局最优。如果一个问题能满足最优化原理,就称其具有最优子结构性质。这是判断问题能否使用动态规划解决的先决条件,如果一个问题不能满足最优化原理,那么这个问题就不适合用动态规划来求解。

但是如果感觉这个理解起来还是很困难,我们还有另一种方法进行判断,就是无后效性。

什么是无后效性呢,就是当前的决策只影响当前的状态不对后续的状态造成影响,而且与到底此状态的方式无关,说简单点就是当前状态所做的决策和选择不影响后续状态,也不受以前的状态影响。

证明最优子结构

书上给出的证明如下 

如果觉得难以理解,接下来我会做出一定的解释。

现在清洗一下脑子,接下来是一段很奇怪的证明方式,我们将引入反证法。(按我的习惯来的,书上用的y,不太一样。)

首先我们假设(x1,x2,…,xn)是01背包问题的最优解,那么其中的每一个x都代表一个子问题的最优解,映射到题目中就是每个物品选择与否。那么则有(x2,x3,…,xn)是其子问题的最优解,现在我们又假设(y2,y3,…,yn)是上述问题的子问题最优解,那么则有

(v2 * y2+v3 * y3+…+vn * yn)+v1 * x1 > (v2 * x2+v3 * x3+…+vn * xn)+v1 * x1

说明(X1,Y2,Y3,…,Yn)才是该01背包问题的最优解,这与最开始的假设(X1,X2,…,Xn)是01背包问题的最优解相矛盾,故01背包问题满足最优性原理。(是不是感觉在鬼扯)

但是证明无后效性就显得很简单,对于任意一个阶段,只要背包剩余容量和可选物品是一样的,那么我们能做出的现阶段的最优选择必定是一样的,是不受之前选择了什么物品所影响的。即满足无后效性

书上的递归关系我会在下面解释。

图解

这是我最不想弄的东西,因为非常的麻烦。

现在有这样一堆宝物(w代表体积,v代表价值)

那么这里我们就需要选择四次,使得x1 * 3 + x2 * 2 + x3 * 5 + x4 * 7 <c(c为背包总容量),使得价值最大。现在假设C为12

首先初始时我们的背包容量还是12,对宝物1只有两种状态,选择或者不选择。

 

 然后依次对4个宝物进行选择就会得到下图。

图中选择宝物2有点弄反了,用软件自动调整了一下就这样了,将就看吧。

 当然这个图是不太正确的,因为在选择宝物三因容积不够选择宝物四的时候就已经是结果了,但是我为了看着方便还是选择迁出一根线来表示,就是最后一行没有红线的所有框框,因为这样我们就把结果全部弄到最后一行了,便于观察。很明显最优的选择是价值为13背包容量为0的那个选择图,即为选择宝物1,2,4不选择宝物3的那条路。

好弄现在通过图像明白了流程,接下来我们就研究一下书上说的递归关系。

递归关系

书中所给为下图 

 这书可真垃圾,让我怀疑自己错了,再三求证才发现是书错了,差评,可见不能尽信书上所言。

这一部分呢其实就是分析咱们每一个状态的选择策略,只是用数学公式化了。

要理解这个需要用到编程的东西具象化。

数组表现形式如下图

书中内容是从左下角求到右上角。

现在有一个二维数组m [ i ] [ j ] ,i 代表的是每个宝物,j 代表的是背包的容积,m [ i ] [ j ]里面存储的就是当前背包内的宝物价值,w代表宝物的容积,v代表宝物的价值。

那么对于当前这个宝物还是只有两个状态,选择或者不选择。i +1是我们已经选择过的宝物,那么立足于当前状态的这个宝物,对于这个宝物的上一个宝物是不是就是 i + 1 所以上面会出现,m [ i+1 ] [ j ] 。

那么现在对于下一个宝物有两种选择(当然能选择肯定是背包的容积还大于宝物的体积的时候)。

不选择那么背包的状态不变,就是m [ i+1 ] [ j ]即上一个宝物的状态

选择,那么背包的状态肯定会变化,首先背包的容积发生改变,即 j 发生改变,该有背包内物品的价值发生改变,就是 m [ i ] [ j ] 内存储的值发生变化,就是m [ i + 1 ] [ j - w [ i ] ] + v [ i ] 

那么对应当前的状态我们需要最佳,肯定是选择最大的那个啊。所以需要用max比较一下,看一下当前的状态哪个最好。

大概就是这样。动态规划最重要的就是递归公式,其实叫递推公式更形象。

代码后续更新。

代码规整版(书上的代码就是函数前面加了book的)

  1. #include <stdio.h>
  2. #include <math.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. void myKnapsack(int *v,int *w,int n,int c);
  6. void traceback(int *w,int n,int c,int*m[n+1][c+1]);
  7. void bookKnapsack(int *v,int *w,int n,int c,int *m[n+1][c+1]);
  8. void booktraceback(int *w,int n,int c,int*m[n+1][c+1]);
  9. int max(int x,int y);
  10. int min(int x,int y);
  11. int main()
  12. {
  13. // int n;
  14. // printf("宝物数量");
  15. // scanf("%d",&n);
  16. // int w[n+1],v[n+1];
  17. // int c;
  18. // printf("背包总量");
  19. // scanf("%d",&c);
  20. // int i=0,j=0;
  21. // int m[n+1][c+1];
  22. // for(i=1;i<n+1;i++)
  23. // {
  24. // printf("请输入第%d个宝物的重量: ",i);
  25. // scanf("%d",&w[i]);
  26. // printf("请输入第%d个宝物的价值: ",i);
  27. // scanf("%d",&v[i]);
  28. // }
  29. // printf("\n");
  30. // for(i=0;i<n+1;i++)
  31. // {
  32. // if(i==0)
  33. // {
  34. // printf("w= ");
  35. // }
  36. // else
  37. // {
  38. // printf("%d ",w[i]);
  39. // }
  40. // }
  41. // printf("\n");
  42. // for(i=0;i<n+1;i++)
  43. // {
  44. // if(i==0)
  45. // {
  46. // printf("v= ");
  47. // }
  48. // else
  49. // {
  50. // printf("%d ",v[i]);
  51. // }
  52. // }
  53. // printf("\n");
  54. // printf("\n");
  55. // myKnapsack(v,w,n,c);
  56. // printf("\n");
  57. int n=4;
  58. int c=10;
  59. int w[]= {0,2,3,5,5};
  60. int v[]= {0,2,4,3,7};
  61. int i,j;
  62. printf("\n");
  63. myKnapsack(v,w,n,c);
  64. printf("\n");
  65. int m[n+1][c+1];
  66. bookKnapsack(v,w,n,c,m);
  67. }
  68. //这个函数思想和我的差不多,也是从最后结果开始查找
  69. void booktraceback(int *w,int n,int c,int*m[n+1][c+1])
  70. {
  71. int i,j;
  72. int x[n];
  73. for(i=1; i<n; i++)
  74. {
  75. if(m[i][c]==m[i+1][c])
  76. {
  77. x[i]=0;
  78. }
  79. else
  80. {
  81. x[i]=1;
  82. c -= w[i];
  83. }
  84. }
  85. x[n]=(m[n][c])?1:0;
  86. for(i=1; i<=n; i++)
  87. {
  88. if(x[i]==1)
  89. {
  90. printf("第%d件宝物被选择\n",i);
  91. }
  92. else
  93. {
  94. printf("第%d件宝物未被选择\n",i);
  95. }
  96. }
  97. }
  98. void bookKnapsack(int *v,int *w,int n,int c,int *m[n+1][c+1])
  99. {
  100. // 找出两个数中比较小的数为下面的模拟装宝物n做准备(就是初始化m数组)。
  101. // 之所以减1是因为j代表的是背包容量,w[n]代表宝物所需容量,背包容量只需要比宝物容量减1大即可。
  102. int jMax = min(w[n] - 1, c);
  103. int i,j;
  104. // 初始化m数组,要是不初始化,后面打印数组时会打印地址出来
  105. for(i=0; i<=n; i++)
  106. {
  107. for(j=0; j<=c; j++)
  108. {
  109. m[i][j]=-1;
  110. }
  111. }
  112. // 这里是模拟装宝物n,如果当前背包容量比宝物n所需容量即装不进背包,顾背包内价值为0
  113. for (j = 0; j<=jMax; j++)
  114. {
  115. m[n][j] = 0;
  116. }
  117. // 这里是装的下宝物n的时候,就装下,背包内价值为v[n]
  118. for (j = w[n]; j <= c; j++)
  119. {
  120. m[n][j] = v[n];
  121. }
  122. // 因为宝物n已经装了,就只需要从n-1 求到2即可,最后一步后面模拟(我也不理解为什么这样,本来直接像我写的代码一直算就行了)
  123. for (i = n -1; i>1; i--)
  124. {
  125. // 这里和上面那个jMax一样
  126. jMax = min(w[i]-1, c);
  127. // 装不下就背包状态不变
  128. for (j = 0; j <= jMax; j++)
  129. {
  130. m[i][j] = m[i + 1][j];
  131. }
  132. // 能装下就进项判断之所以用(int)转换一下,是因为不转换电脑会算错,我也不知道为啥,应该是语言缺陷啥的
  133. for (j = w[i]; j <= c; j++)
  134. {
  135. m[i][j] = max(m[i + 1][j], (int)m[i + 1][j - w[i]]+v[i]);//当前背包容量装得下,但是要判断其价值是否最大,确定到底装不装
  136. }
  137. }
  138. // 这里开始模拟最后一个宝物1的过程
  139. m[1][c] = m[2][c];//先假设1物品不装
  140. if (c >= w[1])
  141. {
  142. m[1][c] = max(m[1][c], (int)m[2][c - w[1]]+v[1]);//根据价值决定到底装不装
  143. }
  144. printf("(书上的)遍历后数组为\n");
  145. for(i=1; i<=n; i++)
  146. {
  147. printf("\n");
  148. for(j=0; j<=c; j++)
  149. {
  150. printf("%-3d ",m[i][j]);
  151. }
  152. }
  153. printf("\n");
  154. printf("最大收益为 %d",m[1][c]);
  155. printf("\n");
  156. booktraceback(w,n,c,m);//调用查找函数
  157. }
  158. int min(int x,int y)
  159. {
  160. if(x<y)
  161. return x;
  162. else if(x>y)
  163. return y;
  164. else
  165. return x;
  166. }
  167. int max(int x,int y)
  168. {
  169. if(x>y)
  170. return x;
  171. else if(x<y)
  172. return y;
  173. else
  174. return x;
  175. }
  176. void traceback(int *w,int n,int c,int*m[n+1][c+1])
  177. {
  178. int i,j;
  179. int choice[n];
  180. for(i=n; i>=1; i--)
  181. {
  182. if(m[i-1][c]!=m[i][c])//第i个物品被选中
  183. {
  184. choice[i]=1;
  185. c=c-w[i];//从m(i-1,j-wi)开始寻找
  186. }
  187. else if (m[i-1][c]=m[i][c])//第i个物品没有被选中
  188. {
  189. choice[i]=0;
  190. }
  191. }
  192. for(i=1; i<=n; i++)
  193. {
  194. if(choice[i]==1)
  195. {
  196. printf("第%d件宝物被选择\n",i);
  197. }
  198. else
  199. {
  200. printf("第%d件宝物未被选择\n",i);
  201. }
  202. }
  203. }
  204. void myKnapsack(int *v,int *w,int n,int c)
  205. {
  206. int m[n+1][c+1];
  207. int i,j,tmp;
  208. for(i=0; i<=n; i++)//将数组的第00列全部初始化为0
  209. {
  210. m[i][0]=0;
  211. }
  212. for(i=0; i<=c; i++)
  213. {
  214. m[0][i]=0;
  215. }
  216. for(i=1; i<=n; i++)//遍历数组开始寻找
  217. {
  218. for(j=1; j<=c; j++)
  219. {
  220. if(j<w[i])//当背包容积小于当前物品重量时背包状态不变
  221. {
  222. m[i][j]=m[i-1][j];
  223. }
  224. else
  225. {
  226. if(m[i-1][j]>m[i-1][j-w[i]]+v[i])//利用递推公式判断是否将物品装进背包
  227. {
  228. m[i][j]=m[i-1][j];
  229. }
  230. else
  231. {
  232. m[i][j]=m[i-1][j-w[i]]+v[i];
  233. }
  234. }
  235. }
  236. }
  237. printf("(我的)遍历后矩阵为\n");
  238. for(i=1; i<=n; i++)
  239. {
  240. for(j=1; j<=c; j++)
  241. {
  242. printf("%-3d ",m[i][j]);
  243. }
  244. printf("\n");
  245. }
  246. printf("\n");
  247. printf("最大价值为%d\n",m[n][c]);
  248. printf("\n");
  249. traceback(w,n,c,m);
  250. }

老师那个vs6老六编译器,不支持变量定义数组大小版本

  1. #include <stdio.h>
  2. #include <math.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. void myKnapsack(int *v,int *w,int n,int c,int m[5][11]);
  6. void traceback(int *w,int n,int c,int m[5][11]);
  7. void bookKnapsack(int *v,int *w,int n,int c,int m[5][11]);
  8. void booktraceback(int *w,int n,int c,int m[5][11]);
  9. int max(int x,int y);
  10. int min(int x,int y);
  11. int max(int x,int y);
  12. int min(int x,int y);
  13. void main()
  14. {
  15. int n=4;
  16. int c=10;
  17. int w[]= {0,2,3,5,5};
  18. int v[]= {0,2,4,3,7};
  19. int i=0,j=0;
  20. int m[5][11];
  21. myKnapsack(v,w,n,c,m);
  22. printf("\n");
  23. int bookm[5][11];
  24. bookKnapsack(v,w,n,c,bookm);
  25. }
  26. void traceback(int *w,int n,int c,int m[11][11])
  27. {
  28. int i=0;
  29. int choice[10];
  30. for(i=n; i>=1; i--)
  31. {
  32. if(m[i-1][c]!=m[i][c])//第i个物品被选中
  33. {
  34. choice[i]=1;
  35. c=c-w[i];//从m(i-1,j-wi)开始寻找
  36. }
  37. else if (m[i-1][c]=m[i][c])//第i个物品没有被选中
  38. {
  39. choice[i]=0;
  40. }
  41. }
  42. for(i=1; i<=n; i++)
  43. {
  44. if(choice[i]==1)
  45. {
  46. printf("第%d件宝物被选择\n",i);
  47. }
  48. else
  49. {
  50. printf("第%d件宝物未被选择\n",i);
  51. }
  52. }
  53. }
  54. void myKnapsack(int *v,int *w,int n,int c,int m[5][11])
  55. {
  56. int i,j;
  57. for(i=0; i<=n; i++)//将数组的第00列全部初始化为0
  58. {
  59. m[i][0]=0;
  60. }
  61. for(i=0; i<=c; i++)
  62. {
  63. m[0][i]=0;
  64. }
  65. for(i=1; i<=n; i++)//遍历数组开始寻找
  66. {
  67. for(j=1; j<=c; j++)
  68. {
  69. if(j<w[i])//当背包容积小于当前物品重量时背包状态不变
  70. {
  71. m[i][j]=m[i-1][j];
  72. }
  73. else
  74. {
  75. if(m[i-1][j]>m[i-1][j-w[i]]+v[i])//利用递推公式判断是否将物品装进背包
  76. {
  77. m[i][j]=m[i-1][j];
  78. }
  79. else
  80. {
  81. m[i][j]=m[i-1][j-w[i]]+v[i];
  82. }
  83. }
  84. }
  85. }
  86. printf("(我的)遍历后矩阵为\n");
  87. for(i=1; i<=n; i++)
  88. {
  89. for(j=1; j<=c; j++)
  90. {
  91. printf("%-3d ",m[i][j]);
  92. }
  93. printf("\n");
  94. }
  95. printf("\n");
  96. printf("最大价值为%d\n",m[n][c]);
  97. printf("\n");
  98. traceback(w,n,c,m);
  99. }
  100. //这个函数思想和我的差不多,也是从最后结果开始查找
  101. void booktraceback(int *w,int n,int c,int m[5][11])
  102. {
  103. int i=0;
  104. int x[5];
  105. for(i=1; i<n; i++)
  106. {
  107. if(m[i][c]==m[i+1][c])
  108. {
  109. x[i]=0;
  110. }
  111. else
  112. {
  113. x[i]=1;
  114. c -= w[i];
  115. }
  116. }
  117. x[n]=(m[n][c])?1:0;
  118. for(i=1; i<=n; i++)
  119. {
  120. if(x[i]==1)
  121. {
  122. printf("第%d件宝物被选择\n",i);
  123. }
  124. else
  125. {
  126. printf("第%d件宝物未被选择\n",i);
  127. }
  128. }
  129. }
  130. void bookKnapsack(int *v,int *w,int n,int c,int m[5][11])
  131. {
  132. // 找出两个数中比较小的数为下面的模拟装宝物n做准备(就是初始化m数组)。
  133. // 之所以减1是因为j代表的是背包容量,w[n]代表宝物所需容量,背包容量只需要比宝物容量减1大即可。
  134. int jMax = min(w[n] - 1, c);
  135. int i,j;
  136. // 初始化m数组,要是不初始化,后面打印数组时会打印地址出来
  137. for(i=0; i<=n; i++)
  138. {
  139. for(j=0; j<=c; j++)
  140. {
  141. m[i][j]=-1;
  142. }
  143. }
  144. // 这里是模拟装宝物n,如果当前背包容量比宝物n所需容量即装不进背包,顾背包内价值为0
  145. for (j = 0; j<=jMax; j++)
  146. {
  147. m[n][j] = 0;
  148. }
  149. // 这里是装的下宝物n的时候,就装下,背包内价值为v[n]
  150. for (j = w[n]; j <= c; j++)
  151. {
  152. m[n][j] = v[n];
  153. }
  154. // 因为宝物n已经装了,就只需要从n-1 求到2即可,最后一步后面模拟(我也不理解为什么这样,本来直接像我写的代码一直算就行了)
  155. for (i = n -1; i>1; i--)
  156. {
  157. // 这里和上面那个jMax一样
  158. jMax = min(w[i]-1, c);
  159. // 装不下就背包状态不变
  160. for (j = 0; j <= jMax; j++)
  161. {
  162. m[i][j] = m[i + 1][j];
  163. }
  164. // 能装下就进项判断之所以用(int)转换一下,是因为不转换电脑会算错,我也不知道为啥,应该是语言缺陷啥的
  165. for (j = w[i]; j <= c; j++)
  166. {
  167. m[i][j] = max(m[i + 1][j], (int)m[i + 1][j - w[i]]+v[i]);//当前背包容量装得下,但是要判断其价值是否最大,确定到底装不装
  168. }
  169. }
  170. // 这里开始模拟最后一个宝物1的过程
  171. m[1][c] = m[2][c];//先假设1物品不装
  172. if (c >= w[1])
  173. {
  174. m[1][c] = max(m[1][c], (int)m[2][c - w[1]]+v[1]);//根据价值决定到底装不装
  175. }
  176. printf("(书上的)遍历后数组为\n");
  177. for(i=1; i<=n; i++)
  178. {
  179. printf("\n");
  180. for(j=0; j<=c; j++)
  181. {
  182. printf("%-3d ",m[i][j]);
  183. }
  184. }
  185. printf("\n");
  186. printf("最大收益为 %d",m[1][c]);
  187. printf("\n");
  188. booktraceback(w,n,c,m);//调用查找函数
  189. }
  190. int min(int x,int y)
  191. {
  192. if(x<y)
  193. return x;
  194. else if(x>y)
  195. return y;
  196. else
  197. return x;
  198. }
  199. int max(int x,int y)
  200. {
  201. if(x>y)
  202. return x;
  203. else if(x<y)
  204. return y;
  205. else
  206. return x;
  207. }

图解

图解(查找是否获取图解即traceback函数)

下面有解释

 图中标黄部分为所利用到的点,根据咱们的递推原理,进行反推其实就能得到哪些宝物被选择,首先从坐标(4,12)开始,就是最后的答案处即(n,c)开始查找(n为宝物个数),如果m(i-1,j)=m(i,j)就说明这个物品没有被选择,那么就需要从m(i-1,j)继续寻找,如果m(i-1,j)!=m(i,j)就说明该物品被选择了,就需要从m(i-1,j-w[i])开始寻找,这里的 i 的含义为第几个宝物。

举个栗子 i = 4,从m[ 4 ][ 12 ]开始寻找m[ 3 ][ 12 ] !=m[ 4 ][ 12 ] 所以宝物4被选择了,接下来需要来到m[ 4 - 1 ] [ c - w[ i ] ]即m[ 4-1 ] [ 12 - 7 ] = m[ 3 ] [ 5 ] 开始寻找。

此时m[ 3 ] [ 5 ] = m [ 2 ] [ 5 ] 所以宝物3没有被选择,就需要从 m [ 2 ] [ 5 ] 开始寻找。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/401602
推荐阅读
相关标签
  

闽ICP备14008679号