实验报告五:用分支限界优化的石板切割问题
一、 实验要求
给出一长度、高度均已知的木板,给出一系列长度、高度的目标板。
要求:
•目标板不能旋转,目标板长宽必须与木板长宽对应。•必须沿长边短边一刀切,每一次切割将木板分割为两个矩形。•目标板的一角应与木板一角重合。•为简化问题,题中所给的长宽均为整数。请给出木板利用率最高的切割方案。•尝试给出可视化方案•使用分支限界优化
二、实验设计
针对本次实验,计划将实验任务分割为两个部分,第一,尝试设计分支限界的优化方案,第二,尝试给出可视化方案。
设计分支限界优化方案
实践表明,我设计的分支限界方案的效率并不高,但是为了获得最优解,我目前很难再找到更好的优化方案,由于本次实验设计中存在枚举分配目标块的举动,不得不将时间复杂度拉高到了指数级,由此可以看出实验三:动态规划已经是一个相当优秀的算法,分支限界的优化相比递归解法有效,但效果有限。
设计限界函数
我设计的限界函数为:当目前可以用的块的面积总和或剩下的木板面积总和(两者取最小值)即使被完全切割,也不再能比现在已有的切割面积更大的时候,说明往后继续搜索是不再有意义的,那么就没有继续搜索的必要了。
由于我们在切割分配的时候,会分为横切与竖切,假如进入横切中的一块进行下一层递归,是不可能知道另外一块切割结果的,因此我们在设计限界函数时应该往最大方向考虑,即,直接认为另外一块被完全切割了【另外一块被完全切割的意思也是另外一块的可以用的块的面积总和或另外一块木板面积(两者取最小值)】,具体代码体现如下:
if(s1_hadused + s1_nowhave > res || s2_hadused + s2_nowhave > res)//不满足限界函数完全无搜索必要
ress = max(ress, stone[i].s + BFSshape(b1, x1, y1, s1_nowhave, s1_hadused, i)
+ BFSshape(b2, x2, y2, s2_nowhave, s2_hadused, i));
解释 if 函数的意思就是如果第一块已经用的,加上第一块还有的或者第二块已经用的加上第二块还有的任意一个还有机会打破res,那就进入下一层搜索。其中:
s1_hadused = S_hadused + min(s2_nowhave, x2 * y2);
s2_hadused = S_hadused + min(s1_nowhave, x1 * y1);
s1_hadused \ s2_hadused 已经按照上面所述规则修改为已经使用的加上剩下面积和与另一块木块面积最小值。
设计函数出口
如果剩余块只有1块,那么不需要再考虑横切与竖切,只要能切,返回这1块的面积,倘若不能切,返回0;
如果剩余块只有0块,那么直接返回0即可。
在每层递归中都需要进行比较,更新该层最大的结果,保存为该层最大值
该层最大值 = max (该层选择切割的块面积 + 子块1返回值 + 子块2返回值);
设计可视化方案
由于python对于图片处理更加方便,这里使用python + opencv来设计可视化方案。
我们确保每次切割都是从左下角进行切割的,因此绘图从左下角作为起点。
我们如果能通过cpp输出以下信息那我们就可以进行计算:
struct Print{
int x;//当前切割块的宽
int y;//当前切割块的高
bool mode; //当前切割的方式,例如,可以把1设置为竖切
};
按顺序输入以上信息,且默认存储时,竖切先左后右,横切先下后上就可以反算每次切割的坐标,就可以作出每次切割的图像。
使用python绘图代码如下:
import matplotlib.pyplot as plt
import numpy as np
import time
from matplotlib.pyplot import MultipleLocator
import pandas as pd
import os
if __name__ == "__main__":
fig1 = plt.figure()
ax1 = fig1.add_subplot(111, aspect='equal')
ax1.add_patch(
plt.Rectangle(
(0, 0), # (x,y)矩形左下角
8, # width长
4, # height宽
color='maroon',
alpha=0.5
)
)
plt.xlim(-1, 6)
plt.ylim(1, 7)
plt.show()
fig1.savefig('rect1.png', dpi=90, bbox_inches='tight')
进行rectangle的解算也是一个递归的过程,传入参数为当前切割块的坐标、上面的struct内的参数。
倘若mode为1,修改当前切割块的坐标,传入下一个struct进入下一层循环,以队列方式添加新块,保证切割过程中与被切割块一一对应。这个函数并不困难,这里略过。
要输出struct中的内容,在前面的实验三、实验四中均可以实现,这里也就不再实现了。
输出PPT上的测试样例如图:
三、代码展示(分支限界)
#include <iostream>
#include <algorithm>
#include <bitset>
using namespace std;
const int N = 101;
typedef struct Stone{
int x;
int y;
int s;
bool operator<(const Stone& st) const
{
return s < st.s;
}
}Stone;
Stone stone[N];
int n;
int X_, Y_;
int res = 0;
int Count = 0;
int BFSshape(bitset<N> available, int X, int Y, int S_nowhave, int S_hadused, int maxp)//以used作为标记序列,在X,Y大小的初始板上完成切割
{
// Count ++;
// if(Count % 100000 == 0) cout << Count << endl;
// cout << available.to_string() << endl;
if(available.none())return 0;
if(available.count() == 1)
{
for(int i = 0; i < maxp; i++)
if(available[i])
{
// cout << X << ' ' << Y << ' ' << stone[i].x << ' ' << stone[i].y << endl;
if(X >= stone[i].x && Y >= stone[i].y)
return stone[i].s;
else return 0;
}
}
int ress = 0;
bitset<N> b1, b2;
for(int i = maxp - 1; i >= 0; i--)
{
if(available[i] && X >= stone[i].x && Y >= stone[i].y)//如果可以切
for(int j = 0; j < 1 << maxp; j++)//逐个枚举切
{
b1 = bitset<N>(j);
if(((b1 ^ available) & b1).none())
{
b2 = ~b1;
b1[i] = b2[i] = false;
for(int i = 0; i < maxp; i++)
if(!available[i]) b2[i] = false;
//横切
int x1 = X - stone[i].x;
int y1 = stone[i].y;
int x2 = X;
int y2 = Y - stone[i].y;
int s1_nowhave = 0, s2_nowhave, s1_hadused, s2_hadused;
for(int k = 0; k < maxp; k ++)
if(b1[k])s1_nowhave += stone[k].s;
s2_nowhave = S_nowhave - s1_nowhave - stone[i].s;
s1_hadused = S_hadused + min(s2_nowhave, x2 * y2);
s2_hadused = S_hadused + min(s1_nowhave, x1 * y1);
if(s1_hadused + s1_nowhave > res || s2_hadused + s2_nowhave > res)//不满足限界函数完全无搜索必要
ress = max(ress, stone[i].s + BFSshape(b1, x1, y1, s1_nowhave, s1_hadused, i)
+ BFSshape(b2, x2, y2, s2_nowhave, s2_hadused, i));
//竖切
x1 = X - stone[i].x;
y1 = Y;
x2 = stone[i].x;
y2 = Y - stone[i].y;
s1_hadused = S_hadused + min(s2_nowhave, x2 * y2);
s2_hadused = S_hadused + min(s1_nowhave, x1 * y1);
if(s1_hadused + s1_nowhave > res || s2_hadused + s2_nowhave > res)//不满足限界函数完全无搜索必要
ress = max(ress, stone[i].s + BFSshape(b1, x1, y1, s1_nowhave, s1_hadused, i)+
BFSshape(b2, x2, y2, s2_nowhave, s2_hadused, i));
if(X == X_ && Y == Y_)res = ress;
}
}
}
return ress;
}
int main()
{
int sums = 0;
cin >> n >> X_ >> Y_;
for(int i = 0; i < n; i++)
{
cin >> stone[i].x >> stone[i].y;
stone[i].s = stone[i].x * stone[i].y;
sums += stone[i].s;
}
sort(stone, stone + n);
bitset<N> used;
used.set();
res = sums;
int re = BFSshape(used, X_, Y_, sums, 0, n);
cout << "MAX_S : " << re << endl;
cout << "RATE : " << (double)re / (X_ * Y_) << endl;
}
四、测试结果
使用实验4中的测试结果:
1.
4
4 8
3 2
2 4
1 6
4 4
输出:
MAX_S : 24
RATE : 0.75
2.
5
4 4
2 2
2 2
2 2
2 2
3 3
输出:
MAX_S : 16
RATE : 1
3.使用100个木块的测试结果,测试发现要出结果可以算到明年,于是就放弃了,算法效率优化有限,可能和限界函数并不优秀有关。
四、总结
算法效率仍然低,并未体现出分支限界的优点,使用到了分支限界的算法,体会了分支限界的思想,但也可见,要能够找到一个优良的分支限界函数又不失最优性是相当不容易的。