当前位置:   article > 正文

回溯算法求解01背包问题_利用回溯法编程求解0-1背包问题

利用回溯法编程求解0-1背包问题

问题描述

01背包问题是算法中的经典问题:
      对于给定的N个物品,第i个物品的重量为wi,价值为vi,对于一个最多能装重量c的背包,应该如何选择放入包中的物品,使得包中物品的总价值最大?

算法分析

简单来说回溯算法=深度优先遍历+剪枝,下面我们进行分析:

01背包属于最优解问题。对于每一个物品i,我们可以选择装入背包或不装入背包。对n个物品,我们顺序依次考虑每个物品,这样就形成了一棵二叉树。基本思想就是深度优先遍历这棵树,在搜索空间树时,左子节点是可一个可行结点,搜索就进入其左子树;对于右子树时,先计算上界函数,以判断是否将其减去(剪枝)。最后保存价值最大的情况,该方案就是最终结果。

时间复杂度:O(2^n)。

代码实现

code 1

  1. #include <stdio.h>
  2. int n; // 物品数量
  3. double c; // 背包容量
  4. double w[100]; // 各个物品的重量 weight
  5. double v[100]; // 各个物品的价值 value
  6. double cw = 0; // 当前背包重量 current weight
  7. double cv = 0; // 当前背包中物品总价值 current value
  8. double bestv = 0; // 当前最优价值 best value
  9. double perv[100]; // 单位物品价值(排序后) per value
  10. int cput[100]; // 当前物品放入情况
  11. int put[100]; // 设置是否装入,为1的时候表示选择该组数据装入,为0的表示不选择该组数据
  12. int i, j; // 循环使用
  13. // 计算上界
  14. double bound(int index) {
  15. int rp = 0;
  16. // 计算剩余物品重量和
  17. while(index <= n) {
  18. rp += v[index];
  19. index++;
  20. }
  21. return cv + rp;
  22. }
  23. // 回溯法
  24. void backtrack(int index) {
  25. // 如果已经处理完所有物品
  26. if (index > n) {
  27. // 记录装入情况
  28. for(i = 1; i <=n; i++) {
  29. put[i] = cput[i];
  30. }
  31. // 更新当前最优价值
  32. bestv = cv;
  33. return;
  34. }
  35. if (cw + w[index] <= c) {
  36. cw += w[index];
  37. cv += v[index];
  38. cput[index] = 1;
  39. backtrack(index + 1);
  40. // 回溯
  41. cw -= w[index];
  42. cv -= v[index];
  43. }
  44. // 如果剩余物品的上界价值大于当前最优价值,尝试不放入
  45. if (bound(index + 1) > bestv) {
  46. cput[index] = 0;
  47. backtrack(index + 1);
  48. }
  49. }
  50. // 01背包问题
  51. void knapsack() {
  52. printf("请输入物品的数量和背包的容量:\n");
  53. scanf("%d %lf", &n, &c);
  54. printf("请输入每个物品的重量:\n");
  55. for (i = 1; i <= n; i++) {
  56. scanf("%lf", &w[i]);
  57. }
  58. printf("请输入每个物品的价值:\n");
  59. for (i = 1; i <= n; i++) {
  60. scanf("%lf", &v[i]);
  61. }
  62. backtrack(1);
  63. printf("最优价值为:\n%.0lf\n", bestv);
  64. printf("最佳选择为:\n");
  65. for (i = 1; i <= n; i++) {
  66. // printf("%d", put[i]);
  67. if(put[i] == 1) {
  68. printf("1\n");
  69. } else {
  70. printf("0\n");
  71. }
  72. }
  73. }
  74. int main() {
  75. knapsack();
  76. }

code2 

以下是贪心策略优化后的代码实现

  1. #include <stdio.h>
  2. int n; // 物品数量
  3. double c; // 背包容量
  4. double w[100]; // 各个物品的重量 weight
  5. double v[100]; // 各个物品的价值 value
  6. double cw = 0; // 当前背包重量 current weight
  7. double cv = 0; // 当前背包中物品总价值 current value
  8. double bestv = 0; // 当前最优价值 best value
  9. double perv[100]; // 单位物品价值(排序后) per value
  10. int cput[100]; // 当前物品放入情况
  11. int put[100]; // 设置是否装入,为1的时候表示选择该组数据装入,为0的表示不选择该组数据
  12. int order[100]; // 物品的编号
  13. int i, j; // 循环使用
  14. // 对输入数据进行排序处理
  15. void initsort() {
  16. for(i = 1; i <= n; i++)
  17. order[i] = i;
  18. int temporder = 0;
  19. double temp = 0.0;
  20. // 计算单位价值(单位重量的物品价值)
  21. for(i = 1; i <= n; i++)
  22. perv[i] = v[i] / w[i];
  23. for(i = 1; i <= n - 1; i++) {
  24. for(j = i + 1; j <= n; j++) {
  25. if(perv[i] < perv[j]) {
  26. // 对单位价值进行排序,同时对对应其他数据排序
  27. temp = perv[i];
  28. perv[i] = perv[j];
  29. perv[j] = temp;
  30. temporder = order[i];
  31. order[i] = order[j];
  32. order[j] = temporder;
  33. temp = v[i];
  34. v[i] = v[j];
  35. v[j] = temp;
  36. temp = w[i];
  37. w[i] = w[j];
  38. w[j] = temp;
  39. }
  40. }
  41. }
  42. }
  43. // 计算上界
  44. double bound(int index) {
  45. // 判断当前背包的总价值cv+剩余容量可容纳的最大价值<=当前最优价值
  46. double leftw = c - cw; // 剩余背包容量
  47. double b = cv; // 记录当前背包的总价值cv,最后求上界
  48. // 以物品单位重量价值递减次序装入物品
  49. while(index <= n && w[index] <= leftw) {
  50. leftw -= w[index];
  51. b += v[index];
  52. index++;
  53. }
  54. //装满背包
  55. if(index <= n)
  56. b += v[index] / w[index] * leftw;
  57. return b;
  58. }
  59. // 回溯法
  60. void backtrack(int index) {
  61. // 如果已经处理完所有物品
  62. if (index > n) {
  63. // 记录装入情况
  64. for(i = 1; i <=n; i++) {
  65. put[i] = cput[i];
  66. }
  67. // 更新当前最优价值
  68. bestv = cv;
  69. return;
  70. }
  71. if (cw + w[index] <= c) {
  72. cw += w[index];
  73. cv += v[index];
  74. cput[index] = 1;
  75. backtrack(index + 1);
  76. // 回溯
  77. cw -= w[index];
  78. cv -= v[index];
  79. }
  80. // 如果剩余物品的上界价值大于当前最优价值,尝试不放入
  81. if (bound(index + 1) > bestv) {
  82. cput[index] = 0;
  83. backtrack(index + 1);
  84. }
  85. }
  86. // 恢复排序
  87. void endsort() {
  88. int temput = 0;
  89. int temporder = 0;
  90. for(i = 1; i <= n - 1; i++) {
  91. for(j = i + 1; j <= n; j++) {
  92. if(order[i] > order[j]) {
  93. temporder = order[i];
  94. order[i] = order[j];
  95. order[j] = temporder;
  96. temput = put[i];
  97. put[i] = put[j];
  98. put[j] = temput;
  99. }
  100. }
  101. }
  102. }
  103. // 01背包问题
  104. void knapsack() {
  105. printf("请输入物品的数量和背包的容量:\n");
  106. scanf("%d %lf", &n, &c);
  107. printf("请输入每个物品的重量:\n");
  108. for (i = 1; i <= n; i++) {
  109. scanf("%lf", &w[i]);
  110. }
  111. printf("请输入每个物品的价值:\n");
  112. for (i = 1; i <= n; i++) {
  113. scanf("%lf", &v[i]);
  114. }
  115. initsort();
  116. backtrack(1);
  117. endsort();
  118. printf("最优价值为:\n%.0lf\n", bestv);
  119. printf("最佳选择为:\n");
  120. for (i = 1; i <= n; i++) {
  121. // printf("%d", put[i]);
  122. if(put[i] == 1) {
  123. printf("1\n");
  124. } else {
  125. printf("0\n");
  126. }
  127. }
  128. }
  129. int main() {
  130. knapsack();
  131. }

样例测试

输入

  1. 10 165
  2. 23 31 29 44 53 38 63 85 89 82
  3. 92 57 49 68 60 43 67 84 87 72
  1. 15 750
  2. 70 73 77 80 82 87 90 94 98 106 110 113 115 118 120
  3. 135 139 149 150 156 163 173 184 192 201 210 214 221 229 240

输出 

  1. 最优价值为:
  2. 309
  3. 最佳选择为:
  4. 1
  5. 1
  6. 1
  7. 1
  8. 0
  9. 1
  10. 0
  11. 0
  12. 0
  13. 0
  1. 最优价值为:
  2. 1458
  3. 最佳选择为:
  4. 1
  5. 0
  6. 1
  7. 0
  8. 1
  9. 0
  10. 1
  11. 1
  12. 1
  13. 0
  14. 0
  15. 0
  16. 0
  17. 1
  18. 1

算法分析

适用性

当需要找出某个问题的解集(全部解)或者要求回答什么解是满足某些约束条件的最优解时,往往要使用回溯法。

注意事项

回溯法涉及到深度优先搜索,递归,以及状态空间树的遍历。需要理解如何构建状态空间树、如何选择分支、如何递归地搜索解空间等,需要对问题进行合理分析及解空间树的抽象化。同时剪枝策略是一项重要的技巧,它可以帮助减小搜索空间,从而提高算法的效率,剪枝的实现也具有一定的难度。

效率

回溯法是一种穷举法,它会遍历所有可能的情况放入组合。因此,其时间复杂度是指数级别的,为O(2^n)。01背包问题中,剩余物品价值上界剪枝和容量剪枝是两个常用的策略,它们可以显著减少搜索的复杂性,提高算法效率。

总结

回溯算法

回溯是一种搜索算法,用于解决组合优化问题。它尝试在问题的解空间中搜索不同的组合,以找到最优解。回溯算法的核心思想是深度优先搜索,递归地探索解空间树的各个分支,同时使用剪枝策略来减少搜索空间,提高效率。

回溯过程

回溯算法的核心是 backtrack 函数,该函数递归地探索解空间,考虑以下情况:

如果已经处理完所有物品,记录当前最优解(装入情况)和更新最优价值。

如果当前物品的重量可以放入背包,选择装入该物品,更新当前背包的重量和总价值,继续处理下一个物品。

如果剩余物品的上界价值仍然大于当前最优价值,尝试不放入当前物品,继续搜索。

上界剪枝

为了提高效率,算法使用上界函数 bound 来估计剩余未处理物品的最大可能总价值。如果该上界小于当前最优解,就不再继续搜索该分支,从而减少搜索空间。

[回溯法是按照深度优先的方式对解空间树(状态空间树)进行搜索,从而求得最优解的算法。可以使用贪心算法对该方法进行优化(见Code2),同时更改剪枝策略,可以更加有效的提高算法的效率。]  

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