实验报告三:动态规划求解石板切割问题
一、 实验要求
给出一长度、高度均已知的木板,给出一系列长度、高度的目标板。
要求:
目标板不能旋转,目标板长宽必须与木板长宽对应。
必须沿长边短边一刀切,每一次切割将木板分割为两个矩形。
目标板的一角应与木板一角重合。
为简化问题,题中所给的长宽均为整数。
请给出木板利用率最高的切割方案。
二、问题分析与方案设计
本题的解决方案是完全原创的。
动态规划状态转移方程寻找
试图寻找子问题,就寻找子问题而言,本题还是非常明显的。
每次从石板切割出一块目标块后,问题就转变为两个子问题:再从剩下的两块子块中选取切割面积最大的方案,但是,两块子块的切割方案不能重复,也不能包含刚切的目标块,这是处理的难点,这个难点体现在了动态规划的表格设计上。
动态规划的表格设计
本次问题难点在于如何设计一个合适的表格,能够恰当地表示递推关系,经过我的思考,策划,选择了许多表格后,我最终找到了以仿照01背包的思想的方式,设计如下表格:
f表格存放: 对于(pxq)的木板,包含第i块目标块时的最大切割面积 | i (0 ,1 , ... ,n )共n列 |
---|---|
(p , q) (p = {1, 2, 3,..., maxp) , q = {1, 2, 3,..., maxq} 共maxp*maxq列 |
于是制作如此一个n * maxp * maxq大小的表格。表格纵向表示maxp*maxq列一切可能遇到的子木板大小,横向表示一定包含第i块目标块切割的最大面积。
推导出状态转移方程:
f[i,j][k] = si(块的面积) + max{(f[竖切块1][不含k的所有项] + f[竖切块2][不含k的所有项](且两者无重复)),(f[横切块1][不含k的所有项] + f[横切块2][不含k的所有项](且两者无重复))}
不含k的所有项是指从1~n的所有选择,再取max,对应代码中ii jj 的作用。
通过状态转移方程,我们发现,每一个子问题长宽均在减小,于是我们可以通过横向填表的方式完成表格。
下面证明其最优子结构性质 :
使用反证法,倘若子结构不是符合条件的子结构中最优的,我们一定可以再找到符合条件中更优的子结构来替换它,从而使f[i, j] [k]更优。因此,该表格建立方式具有最优子结构性质。
边界条件:
倘若我们选择了某一块后,已经不再有满足条件的子结构,那么,当前选择此块的最大解就应该为si,倘若某块选择后,发现并不能被选择,此块无法切割,那么,当前选择此块的最大解应该为0。
为了简化制表,我在实际操作中,使用三维数组,保存表格信息,分别为f [i] [j] [k]。
取结果:
最后的结果存在于 f [maxi] [maxj] 一行中 最大值处。
如何寻找出目标块是否被使用过
有了以上思路之后会发现,寻找目标块是否被使用过又成了这个问题的一大难点,结合前几天学习的bitset,以及以前学习的二进制枚举的知识,我想出了使用二进制作为选中标记,使用位运算完成查重的办法。
设计目标块使用记录的备忘录表格
目标块使用记录的备忘录表格通过开设一个与f[i] [j] [k]等大的bitset数组 b[i] [j] [k]来实现,每一个对应的位置记录如上图所示的目标块使用情况,每次使用状态转移方程进行递归运算之前,均需要进行b[i] [j] [k] 的可行性检验,只有检验通过了,才应该作为符合切割要求的子情况之一加入考虑,才能继续计算该情况的最大面积并与最大值作比较。
b表格存放: 对于(pxq)的木板,包含第i块目标块时的最大切割面积 | i (0 ,1 , ... ,n )共n列 |
---|---|
(p , q) (p = {1, 2, 3,..., maxp) , q = {1, 2, 3,..., maxq} 共maxp*maxq列 |
做了如上思考后,就可以开始着手写代码了。
子函数select代码设计
子函数select包含了枚举、筛选、求最大面积的功能。它具有返回值,返回当前切割的最大值。
在该函数中完成b[i] [j] [k]的填表。
int select(int i, int j, int k, int n)//填写第(i,j,k)处表格,最大个数为n
{
bitset<100> tmp;//新建一个临时bitset
int max = 0, tt;
for(int ii = 1; ii <= n; ii ++)
for(int jj = 1; jj <= n; jj ++)//枚举,根据状态转移方程,需要枚举所有组合
{
//竖切
tmp = b[i][j - h[k]][ii] & b[i - w[k]][h[k]][jj];//tmp通过竖切关系式作按位与运算
if(tmp.none() && b[i][j - h[k]][ii][k] == 0 && b[i - w[k]][h[k]][jj][k] == 0)//合法性检验
{
tt = f[i][j - h[k]][ii] + f[i - w[k]][h[k]][jj];//合法,有资格去与max比较大小
if(tt >= max){ max = tt; b[i][j][k] = b[i][j - h[k]][ii] | b[i - w[k]][h[k]][jj]; b[i][j][k][k] = 1;}//倘若max被更新,使用按位或完成使用块的归并操作
}
//横切
tmp = b[i - w[k]][j][ii] & b[w[k]][j - h[k]][jj];//按位与
if(tmp.none() && b[i - w[k]][j][ii][k] == 0 && b[w[k]][j - h[k]][jj][k] == 0)//合法性
{
tt = f[i - w[k]][j][ii] + f[w[k]][j - h[k]][jj];
if(tt >= max){ max = tt; b[i][j][k] = b[i - w[k]][j][ii] | b[w[k]][j - h[k]][jj]; b[i][j][k][k] = 1;}//归并
}
}
return max;//返回最大面积,填好了b[i][j][k];
}
主函数代码设计:
主函数中完成f[i] [j] [k]表格的填写 、完成输入输出。
int main()
{
int n, mxi, mxj, maxs = 0, pos;
cin >> n >> mxi >> mxj;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> h[i];//录入完成
for(int i = 1; i <= mxi; i++)
for(int j = 1; j <= mxj; j++)
for(int k = 1; k <= n; k++)//横向填表
if (i < w[k] || j < h[k]) f[i][j][k] = 0;//含k无法切,最大面积为0;
else f[i][j][k] = w[k] * h[k] + select(i, j, k, n);//可以切,进入状态转移方程
for(int i = 1; i <= n; i++)
if(f[mxi][mxj][i] > maxs) { maxs = f[mxi][mxj][i], pos = i;}
cout << "maximum area:" << maxs << " Utilization rate: " << (double)maxs/(double)(mxi * mxj) << endl;
cout << "success stones:" << endl;
for(int i = 1; i <= n; i++)
if(b[mxi][mxj][pos][i]) cout << "number:" << i << " h&w: " << w[i] << ' ' << h[i] << endl;
//输出部分,依次输出:使用的最大面积,使用率,成功切割的目标板编号及长宽。
}
三、实验代码完全展示
虽然实验思想复杂,但实现起来代码不超过50行,核心代码不超过20行,体现了动态规划算法难懂但易写的特性。
#include <iostream>
#include <bitset>
using namespace std;
int f[105][105][105];
int w[105], h[105];
bitset<100> b[105][105][105];
int select(int i, int j, int k, int n)
{
bitset<100> tmp;
int max = 0, tt;
for(int ii = 1; ii <= n; ii ++)
for(int jj = 1; jj <= n; jj ++)
{
//竖切
tmp = b[i][j - h[k]][ii] & b[i - w[k]][h[k]][jj];
if(tmp.none() && b[i][j - h[k]][ii][k] == 0 && b[i - w[k]][h[k]][jj][k] == 0)
{
tt = f[i][j - h[k]][ii] + f[i - w[k]][h[k]][jj];
if(tt >= max){ max = tt; b[i][j][k] = b[i][j - h[k]][ii] | b[i - w[k]][h[k]][jj]; b[i][j][k][k] = 1;}
}
//横切
tmp = b[i - w[k]][j][ii] & b[w[k]][j - h[k]][jj];
if(tmp.none() && b[i - w[k]][j][ii][k] == 0 && b[w[k]][j - h[k]][jj][k] == 0)
{
tt = f[i - w[k]][j][ii] + f[w[k]][j - h[k]][jj];
if(tt >= max){ max = tt; b[i][j][k] = b[i - w[k]][j][ii] | b[w[k]][j - h[k]][jj]; b[i][j][k][k] = 1;}
}
}
return max;
}
int main()
{
int n, mxi, mxj, maxs = 0, pos;
cin >> n >> mxi >> mxj;
for(int i = 1; i <= n; i ++)
cin >> w[i] >> h[i];//录入完成
for(int i = 1; i <= mxi; i++)
for(int j = 1; j <= mxj; j++)
for(int k = 1; k <= n; k++)
if (i < w[k] || j < h[k]) f[i][j][k] = 0;//含k无法切,最大面积为0;
else f[i][j][k] = w[k] * h[k] + select(i, j, k, n);
for(int i = 1; i <= n; i++)
if(f[mxi][mxj][i] > maxs) { maxs = f[mxi][mxj][i], pos = i;}
cout << "maximum area:" << maxs << " Utilization rate: " << (double)maxs/(double)(mxi * mxj) << endl;
cout << "success stones:" << endl;
for(int i = 1; i <= n; i++)
if(b[mxi][mxj][pos][i]) cout << "number:" << i << " h&w: " << w[i] << ' ' << h[i] << endl;
}
四、运行测试
使用PPT上的测试数据。
测试数据输入:
4
4 8
3 2
2 4
1 6
4 4
测试数据输出:
maximum area:24 Utilization rate: 0.75
success stones:
number:2 h&w: 2 4
number:4 h&w: 4 4
得到结果:最大使用面积24, 使用率0.75, 成功切割第2、4号目标板,长宽分别为2 * 4, 4 * 4。
可见得到的结果是正确的,倘若我们将表格打印出来,能更加直观地看到运行过程。(b只截取了需要的4位)
i j k f b
1 1 1 0 0000 1 1 2 0 0000 1 1 3 0 0000 1 1 4 0 0000 1 2 1 0 0000 1 2 2 0 0000 1 2 3 0 0000 1 2 4 0 0000 1 3 1 0 0000 1 3 2 0 0000 1 3 3 0 0000 1 3 4 0 0000 1 4 1 0 0000 1 4 2 0 0000 1 4 3 0 0000 1 4 4 0 0000 1 5 1 0 0000 1 5 2 0 0000 1 5 3 0 0000 1 5 4 0 0000 1 6 1 0 0000 1 6 2 0 0000 1 6 3 6 0100 1 6 4 0 0000 1 7 1 0 0000 1 7 2 0 0000 1 7 3 6 0100 1 7 4 0 0000 1 8 1 0 0000 1 8 2 0 0000 1 8 3 6 0100 1 8 4 0 0000
2 1 1 0 0000 2 1 2 0 0000 2 1 3 0 0000 2 1 4 0 0000 2 2 1 0 0000 2 2 2 0 0000 2 2 3 0 0000 2 2 4 0 0000 2 3 1 0 0000 2 3 2 0 0000 2 3 3 0 0000 2 3 4 0 0000 2 4 1 0 0000 2 4 2 8 0010 2 4 3 0 0000 2 4 4 0 0000 2 5 1 0 0000 2 5 2 8 0010 2 5 3 0 0000 2 5 4 0 0000 2 6 1 0 0000 2 6 2 8 0010 2 6 3 6 0100 2 6 4 0 0000 2 7 1 0 0000 2 7 2 8 0010 2 7 3 6 0100 2 7 4 0 0000 2 8 1 0 0000 2 8 2 8 0010 2 8 3 6 0100 2 8 4 0 0000
3 1 1 0 0000 3 1 2 0 0000 3 1 3 0 0000 3 1 4 0 0000 3 2 1 6 0001 3 2 2 0 0000 3 2 3 0 0000 3 2 4 0 0000 3 3 1 6 0001 3 3 2 0 0000 3 3 3 0 0000 3 3 4 0 0000 3 4 1 6 0001 3 4 2 8 0010 3 4 3 0 0000 3 4 4 0 0000 3 5 1 6 0001 3 5 2 8 0010 3 5 3 0 0000 3 5 4 0 0000 3 6 1 14 0011 3 6 2 14 0110 3 6 3 14 0110 3 6 4 0 0000 3 7 1 14 0011 3 7 2 14 0110 3 7 3 14 0110 3 7 4 0 0000 3 8 1 20 0111 3 8 2 14 0110 3 8 3 20 0111 3 8 4 0 0000
4 1 1 0 0000 4 1 2 0 0000 4 1 3 0 0000 4 1 4 0 0000 4 2 1 6 0001 4 2 2 0 0000 4 2 3 0 0000 4 2 4 0 0000 4 3 1 6 0001 4 3 2 0 0000 4 3 3 0 0000 4 3 4 0 0000 4 4 1 6 0001 4 4 2 8 0010 4 4 3 0 0000 4 4 4 16 1000 4 5 1 6 0001 4 5 2 8 0010 4 5 3 0 0000 4 5 4 16 1000 4 6 1 22 1001 4 6 2 14 0110 4 6 3 20 0111 4 6 4 22 1001 4 7 1 22 1001 4 7 2 14 0110 4 7 3 20 0111 4 7 4 22 1001 4 8 1 20 0111 4 8 2 24 1010 4 8 3 20 0111 4 8 4 24 1010