一、实验要求
给出一长度、高度均已知的木板,以及一系列长度、高度已知的目标板。
要求:
① 目标板长宽要对齐木板长宽,不能旋转。
② 必须沿长边或短边一刀切,将木板分割为两个矩形。
③ 目标板的一角应与木板一角重合。
④ 目标板大小均不同,长度都为整数。
请给出使得木板利用率最高的切割方案。
二、问题分析与方案设计
•
切割部分
想要从木板中切割出一个图形,至少需要两刀,而这两刀有着不同的切法,且不同的切法会影响到最终结果。
如上图,对图中绿色块进行切分,可以第一刀沿竖直方向切割,第二刀沿着水平方向切割(way1),或者相反,第一刀沿水平方向 切割,第二刀沿着竖直方向切割。所以,我们求面积最大值时应该在两种切割方向求最大值。
max(sharp(x, y - aims[i].second, x - aims[i].first, aims[i].second, tmp, ways, used + aims[i].first * aims[i].second), sharp(x - aims[i].first, y, aims[i].first, y - aims[i].second, tmp, ways, used + aims[i].first * aims[i].second));
以上为实际实验方案中的代码表示。
•
划分
要考虑以上两种方案,划分则显得稍微复杂,并非简单的递归那么简单,这是因为划分为两块后,两块的目标块是共用的,不能等到第一块切割完后又按照上一层的目标块去处理另一块,如果这样做,则会导致结果偏大。
由于目标块互斥的属性,我做了如下考虑:
依照二进制枚举的思想,对所有目标块进行划分,划分为两个组,然后对所有组,再去求它们的最大值。
具体想法:
①由数学推导,n个元素具有2^n^种分组方法,于是可以令一个数i为2^n^-1;
②引入一个变量sym = 1;
③使得i与sym作按位与运算,为1,进入t1板,为2,进入t2板;
④迭代,sym左移一位,完成数字i的分组;
⑤上一组计算完成后,数字 i--;
当数字i减至-1时,完成所有枚举。
以下为实现:
int sharp(int x1, int y1, int x2, int y2, vector<pair<int, int>> aims, vector<pair<int, int>> ways, int _used)
{
//该重载sharp函数的作用是,利用二进制进行分流枚举
int usemax = 0, sym = 1, used; //usedmax 用于记录最大值,sym用于二进制枚举移动,used获取递归返回值
vector<pair<int, int>> t1, t2; //t1,t2 作为两个新动态数组存放枚举后目标
int i = pow(2, aims.size()) - 1; //当有i个元素时,由二进制枚举,共有2^i种划分
for (; i != -1; i--)
{
sym = 1;
t1.clear();
t2.clear();
for (int j = 0; j < aims.size(); j++)
{
i &sym ? t1.push_back(aims[j]) : t2.push_back(aims[j]);
sym <<= 1;
} //二进制枚举,公众号推文前有发过
used = sharp(x1, y1, t1, ways, _used) + sharp(x2, y2, t2, ways, _used);
//进入分流后
if (used > usemax)
usemax = used;
//记录并返回面积最大值
}
return usemax;
}
•
跟踪
由题目要求,我们知道,题目还需要我们给出具体的切割方案,所以,在递归过程中,还要进行跟踪。
这一块我思考了很久才解决,具体的解决方案是:
①开一个数组,数组长度小于木板面积,数组类型为vector;
②设置跟踪的递归出口为:当再也无法继续切割时,以当前的面积s作为数组的角标,记录下具体切割的顺序。
最后,通过返回的最大面积,遍历 数组[maxs] ;读出切割顺序。
vector<pair<int, int>> way[1000]; //用于记录对应S取值的路径
•
切割函数
在做了以上优化之后,切割函数只需要做一件事:给出木板的x,y及目标块序列,返回最大面积,并设置跟踪的递归出口。
如何判断一个目标块能不能切以及跟踪出口如何设立?
可以通过判断目标块的长短边是否都 小于木块的长短边,并设置flag,倘若flag始终没有变化,说明所有目标块均不能切,为跟踪的递归出口条件。
int sharp(int x, int y, vector<pair<int, int>> aims, vector<pair<int, int>> ways, int used)
{
//有x,y,aims,求最大使用面积并返回
vector<pair<int, int>> tmp = aims; //tmp数组作为临时数组用于剔除已经切割的目标
int use, usemax = 0, flag = 1;//路径记录的标志:倘若全部无法切割,说明到达一条路径末
for (int i = 0; i < aims.size(); i++)
{
if (aims[i].first > x || aims[i].second > y)
continue;
else
{
flag = 0;
tmp.erase(tmp.begin() + i);
ways.push_back(aims[i]);
use = aims[i].first * aims[i].second + max(sharp(x, y - aims[i].second, x - aims[i].first, aims[i].second, tmp, ways, used + aims[i].first * aims[i].second),
sharp(x - aims[i].first, y, aims[i].first, y - aims[i].second, tmp, ways, used + aims[i].first * aims[i].second));
if (use >= usemax)
usemax = use;
}
ways.pop_back();//重新开始循环前,需要把剔除的拿回,重置条件
tmp = aims;
}
if (flag)
way[used] = ways;
return usemax;
}
三、代码实现
完成以上分析后,就得到了代码实现方案
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
vector<pair<int, int>> way[1000]; //用于记录对应S取值的路径
int sharp(int x, int y, vector<pair<int, int>> aims, vector<pair<int, int>> ways, int used);
int sharp(int x1, int y1, int x2, int y2, vector<pair<int, int>> aims, vector<pair<int, int>> ways, int _used)
{
//该重载sharp函数的作用是,利用二进制进行分流枚举
int usemax = 0, sym = 1, used; //usedmax 用于记录最大值,sym用于二进制枚举移动,used获取递归返回值
vector<pair<int, int>> t1, t2; //t1,t2 作为两个新动态数组存放枚举后目标
int i = pow(2, aims.size()) - 1; //当有i个元素时,由二进制枚举,共有2^i种划分
for (; i != -1; i--)
{
sym = 1;
t1.clear();
t2.clear();
for (int j = 0; j < aims.size(); j++)
{
i &sym ? t1.push_back(aims[j]) : t2.push_back(aims[j]);
sym <<= 1;
} //二进制枚举,公众号推文前有发过
used = sharp(x1, y1, t1, ways, _used) + sharp(x2, y2, t2, ways, _used);
//进入分流后
if (used > usemax)
usemax = used;
//记录并返回面积最大值
}
return usemax;
}
int sharp(int x, int y, vector<pair<int, int>> aims, vector<pair<int, int>> ways, int used)
{
//有x,y,aims,求最大使用面积并返回
vector<pair<int, int>> tmp = aims; //tmp数组作为临时数组用于剔除已经切割的目标
int use, usemax = 0, flag = 1;//路径记录的标志:倘若全部无法切割,说明到达一条路径末
for (int i = 0; i < aims.size(); i++)
{
if (aims[i].first > x || aims[i].second > y)
continue;
else
{
flag = 0;
tmp.erase(tmp.begin() + i);
ways.push_back(aims[i]);
use = aims[i].first * aims[i].second + max(sharp(x, y - aims[i].second, x - aims[i].first, aims[i].second, tmp, ways, used + aims[i].first * aims[i].second),
sharp(x - aims[i].first, y, aims[i].first, y - aims[i].second, tmp, ways, used + aims[i].first * aims[i].second));
if (use >= usemax)
usemax = use;
}
ways.pop_back();//重新开始循环前,需要把剔除的拿回,重置条件
tmp = aims;
}
if (flag)
way[used] = ways;
return usemax;
}
int main()
{
vector<pair<int, int>> aims;
vector<pair<int, int>> ways;
aims.push_back(make_pair(2, 3));
aims.push_back(make_pair(4, 2));
aims.push_back(make_pair(4, 4));
aims.push_back(make_pair(6, 1));
int maxs;
maxs = sharp(8, 4, aims, ways, 0);
cout << maxs << endl;
for (int i = 0; i < way[maxs].size(); i++)
cout << way[maxs][i].first << ',' << way[maxs][i].second << endl;
}
四、实验演示
实验示例数据使用老师ppt所给的测试数据。
int main()
{
vector<pair<int, int>> aims;
vector<pair<int, int>> ways;
aims.push_back(make_pair(2, 3));
aims.push_back(make_pair(4, 2));
aims.push_back(make_pair(4, 4));
aims.push_back(make_pair(6, 1));
int maxs;
maxs = sharp(8, 4, aims, ways, 0);
cout << maxs << endl;
for (int i = 0; i < way[maxs].size(); i++)
cout << way[maxs][i].first << ',' << way[maxs][i].second << endl;
}
实验输出:
24
4,4
4,2
经验算,24/32 = 75%与答案一致
并给出切割方案为,先切割(4,4)的目标块,再切割(4,2)的目标块。
五、尚可优化的地方
•可以通过比较max中元素的大小,确定切割是横切还是竖切,由于添加过程并不算困难,暂且先略过。•倘若目标块不沿角,该算法还无法解决,普适性较差,但能满足题目要求。