当前位置:   article > 正文

实验报告四:用贪心算法优化的石板切割问题

板材切割算法 穷举

实验报告四:用贪心算法优化石板切割问题

一、 实验要求

给出一长度、高度均已知的木板,给出一系列长度、高度的目标板。

要求:

•目标板不能旋转,目标板长宽必须与木板长宽对应。•必须沿长边短边一刀切,每一次切割将木板分割为两个矩形。•目标板的一角应与木板一角重合。•为简化问题,题中所给的长宽均为整数。请给出木板利用率最高的切割方案。

二、问题分析与方案设计

本题的解决方案是完全原创的。

贪心算法设计综述

贪心是一种通过寻求局部最优解来获得全局最优解(或全局最优近似解)的算法,因此,根据贪心的定义很容易联想到此题的贪心解法为:将所有小目标块按照面积排序,根据面积大小,进行选择,每一步选择当前最大的一个,从而每一步都是局部最优解,这个算法看起来正确,但细细深究,其实不然,如下图:

bba3e3455cab1a4111551287613e762f.png

若要在橙色的木板上切割①②③④⑤五个石板,显然,石板⑤是最大的,但倘若第一步就使用贪心算法,会立即选择⑤,而得不到其他可能的结果,从图像中可以看出,直接选择⑤得到的切割率小于100%,而选择①②③④得到的切割率却为100%,因此得到了一个错误的答案,这个特例告诉我们,不能直接使用选取最大面积的方式作为贪心策略。

经过进一步思考,我发现,特例中一开始直接选择⑤会导致其他目标板失去“出场机会”,有没有办法去保证其他目标板的“出场机会”呢?其实是有的。

每一种可行的切割方案中,一定可以找出一块最大的块,最佳可行的切割方案中的最大的目标块,并不一定是所有目标块中最大的目标块,因此,我们可以考虑对所有目标块使它作为最大块,然后进行枚举,这样操作后,我们保证了每一块都能有至少一次“出场机会”,然后对他们取最大值,就可以得到贪心策略算法的(近似)最优解。这样的枚举方式涵盖了所有可能的切割方法。

这样得到的最优解是全局最优解吗?由于纵切与横切的存在,本实验报告代码得到的并非全局最优解,与张德富老师的论文结果进行比较,得到在50、100等级的数据量上,得到的结果误差小于5%,甚至在某些情况下得到了更优的解决方案,但在10000数量级的目标块下,张德富老师的有一个数据可以得到100% 的测试结果,而本算法只能得到65.9%,至于这个问题,可以参考深度学习的思想去尝试进行解决,通过增加运算量来尽可能的获得全局最优解,这一部分将其写在了未进行的优化部分中。

结构

本题我使用了两个struct结构体、两个子函数、使用到了STL中的vector、bitset和sort排序算法,下面一一介绍他们的功能:


struct Stone:

  1. struct Stone
  2. {
  3. int x, y;
  4. int s;
  5. bool operator<(const Stone &s2) const
  6. {
  7. return s < s2.s;
  8. }
  9. } stone[N];

这一个结构体用于存放目标块的信息,且其中重载了小于运算符,使得stone数组可以使用sort算法直接排序。


struct subStone:

  1. struct subStone
  2. {
  3. Stone substone[4];
  4. bitset<4> key;
  5. };

这一个结构体用于存放切割下来的四个子块以及他们的“开关”


两个STL容器的作用:

  1. bitset : 用于表示已经切割完毕的石板编号
  2. vector : 建立subStone数组,存放当前有的子板

子函数divide:

  1. bool divide(int i)
  2. {
  3. if (st.empty())
  4. return false;
  5. for (int k = st.size() - 1; k >= 0; k--) //从小往大
  6. for (int j = 0; j < 4; j++)
  7. {
  8. if (st[k].key[j])
  9. ;
  10. else
  11. continue;
  12. if (st[k].substone[j].x < stone[i].x || st[k].substone[j].y < stone[i].y)
  13. continue;
  14. //能切
  15. if (j == 0 || j == 1)
  16. st[k].key[2] = st[k].key[3] = 0; //切割方式固定了
  17. else
  18. st[k].key[0] = st[k].key[1] = 0;
  19. subStone tmp;
  20. tmp.substone[0].x = st[k].substone[j].x;
  21. tmp.substone[0].y = tmp.substone[2].y = st[k].substone[j].y - stone[i].y;
  22. tmp.substone[1].x = tmp.substone[3].x = st[k].substone[j].x - stone[i].x;
  23. tmp.substone[1].y = stone[i].y;
  24. tmp.substone[2].x = stone[i].x;
  25. tmp.substone[3].y = st[k].substone[j].y;
  26. tmp.key.set();
  27. st.push_back(tmp);
  28. st[k].key[j] = 0;
  29. return true;
  30. }
  31. return false;
  32. }

divide的功能是尝试对目标板i在当前的子板范围内进行切割,倘若能够切割返回true并修改相关子板信息,倘若不能切割,则返回false;


select子函数:

  1. int select(int i, int mxx, int mxy)
  2. {
  3. used.reset();
  4. st.clear();
  5. int s = 0;
  6. if (mxx < stone[i].x || mxy < stone[i].y)
  7. return 0; //最大的不能被选中,return 0
  8. subStone tmp;
  9. tmp.substone[0].x = mxx;
  10. tmp.substone[0].y = mxy;
  11. tmp.key[0] = 1;
  12. st.push_back(tmp);
  13. for (int k = i; k >= 0; k--)
  14. if(divide(k))
  15. {
  16. s += stone[k].s;
  17. used[k] = 1;
  18. }
  19. return s;
  20. }

select子函数用于返回以板块i为最大板块时可以切割的最大面积。


特殊算法解释

如何完成横切与竖切

本实验中,不再特别地考虑横切与竖切,只考虑能否将当前的最大石板完成切割,那具体如何进行?如下图:

227178623d13012cd68f92059b85ee9c.png

由结 构体su bStone中,我们设置了4个石板的存放位置,4个石板分别为横竖割可能的切割结果,然后使用bitset作为“开关”,当bitset为1时表示该石板存在,当对应bitset为0时表示石板不存在,在搜索时应该跳过,石板存在与不存在应该满足以下条件:

•石板刚切完形成的4个新小石板均是存在的(对应横竖切都是可能的)•下一次循环检查后,倘若能够使用小石板,小石板被切割,状态应该修改为不存在•一旦有小石板因为切割不存在,那么,根据上一块小石板产生的原因,另外一种方式产生的小石板将标记为不存在,即,倘若substone[0],substone[1]被使用,substone[2],substone[3]将立即被标记为不存在。

如何完成石板选择

完成石板选择依照:“目标板stone从大选到小,待切板subStone从新选到旧”的原则进行,即遍历stone和vector数组时均按照从后往前的顺序进行。进入到每一个subStone根据其已经标记好的“开关”依次进入到每一个板中,再进行切割,形成新的子板,再加入到vector数组中,直至不再有石板或目标板。

完全代码展示(含注释)

  1. #include <iostream>
  2. #include <algorithm>
  3. #include <bitset>
  4. #include <vector>
  5. using namespace std;
  6. const int N = 100;//根据不同的测试数据大小选择不同的N值
  7. struct Stone
  8. {
  9. int x, y;//记录一块石板的长宽
  10. int s;//记录一块石板的面积
  11. bool operator<(const Stone &s2) const
  12. {
  13. return s < s2.s;
  14. }//重载小于运算符
  15. } stone[N];
  16. struct subStone
  17. {
  18. Stone substone[4];
  19. bitset<4> key;
  20. };
  21. bitset<N> used;//记录使用过的石板
  22. vector<subStone> st;//记录当前所有石板
  23. bool divide(int i)
  24. {
  25. if (st.empty())
  26. return false;//如果石板已经是空的,那就返回无法切割
  27. for (int k = st.size() - 1; k >= 0; k--) //从小往大
  28. for (int j = 0; j < 4; j++)
  29. {
  30. if (st[k].key[j])
  31. ;
  32. else
  33. continue;//如果key检查该板不存在,跳过
  34. if (st[k].substone[j].x < stone[i].x || st[k].substone[j].y < stone[i].y)
  35. continue;//如果不能切,跳过
  36. //能切
  37. if (j == 0 || j == 1)
  38. st[k].key[2] = st[k].key[3] = 0; //切割方式固定了
  39. else
  40. st[k].key[0] = st[k].key[1] = 0;//横竖切只能存在一种
  41. subStone tmp;
  42. tmp.substone[0].x = st[k].substone[j].x;
  43. tmp.substone[0].y = tmp.substone[2].y = st[k].substone[j].y - stone[i].y;
  44. tmp.substone[1].x = tmp.substone[3].x = st[k].substone[j].x - stone[i].x;
  45. tmp.substone[1].y = stone[i].y;
  46. tmp.substone[2].x = stone[i].x;
  47. tmp.substone[3].y = st[k].substone[j].y;
  48. tmp.key.set();//1111
  49. st.push_back(tmp);//以上步骤均为给tmp(新的子板)赋值
  50. st[k].key[j] = 0;
  51. return true;//能切,切一次就行,返回true
  52. }
  53. return false;
  54. }
  55. int select(int i, int mxx, int mxy)
  56. {
  57. used.reset();
  58. st.clear();//初始化
  59. int s = 0;
  60. if (mxx < stone[i].x || mxy < stone[i].y)
  61. return 0; //最大的不能被选中,return 0
  62. subStone tmp;
  63. tmp.substone[0].x = mxx;
  64. tmp.substone[0].y = mxy;
  65. tmp.key[0] = 1;
  66. st.push_back(tmp);//放入第一块子板
  67. for (int k = i; k >= 0; k--)//遍历所有子板
  68. if(divide(k))
  69. {
  70. s += stone[k].s;
  71. used[k] = 1;
  72. }
  73. return s;//返回贪心最优解
  74. }
  75. int main()
  76. {
  77. int n, maxx, maxy;
  78. cin >> n;
  79. cin >> maxx >> maxy;
  80. for (int i = 0; i < n; i++)
  81. {
  82. cin >> stone[i].x >> stone[i].y;
  83. stone[i].s = stone[i].x * stone[i].y;
  84. }
  85. //数据录入完成
  86. sort(stone, stone + n);//排序完成
  87. int maxs = 0;
  88. bitset<N> fused;//用于给used数组做备份
  89. for (int i = n - 1; i >= 0; i--)
  90. {
  91. int tmp = select(i, maxx, maxy);
  92. if (tmp > maxs)
  93. {
  94. maxs = tmp;
  95. fused = used;
  96. }
  97. }//每一次循环都得到以板块i为最大板块时的最优解
  98. cout << maxs << ' ' << fused.to_string() << endl;
  99. cout << ((double)maxs/((double)maxx*maxy));
  100. //输出结果
  101. }

未进行的优化

•我们可以在Stone 结构体中添加从第一块目标块到第k块目标块的面积和,倘若我们求出的最大面积已经大于或等于了当前的目标和,说明,我们即使将所有比第k块小的目标块都切割,也得不到比当前结果更优的解,就可以直接输出结果,该逻辑实现并不困难,所以并未重新在代码中实现。•我们也可以在subStone结构体中添加一个标记量,从而可以记录在此处是横切还是纵切,然后通过一个觅踪函数,就可以输出每一块石板的切割方式以及切割路径。•为了提高得到全局最优解的概率,我们可以在审查subStone.key时引入随机数,使得我们不一定总是从横切开始检查,使得我们可以在横切竖切中随机优先检查,这样,可能得到横切竖切的不同路径,而本算法若要同时检查横切竖切,则会增加大量的复制与递归调用,我认为这可能使得算法丢失了贪心选择性质。

测试

1.使用书上的测试数据

76d35cdb5d6715dd1b3f45550016fd26.png

测试数据输入:

  1. 4
  2. 4 8
  3. 3 2
  4. 2 4
  5. 1 6
  6. 4 4

测试数据输出:

  1. 24 1100
  2. 0.75

 2.使用上述例子的测试数据测试数据输入:

  1. 5
  2. 4 4
  3. 2 2
  4. 2 2
  5. 2 2
  6. 2 2
  7. 3 3

测试数据输出:

  1. 16 01111
  2. 1

    3.使用50、 100 、 10000数据量的测试数据及与张德福老师论文的比较结果

数据量本实验算法结果张德福老师的算法最佳结果
500.9318750.3291(偏差较大,存疑)
1000.9004670.9213
100000.6585331.000000
声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号