算法:使用动态规划求解石板切割问题

实验报告三:动态规划求解石板切割问题

一、 实验要求

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

要求:

  • 目标板不能旋转,目标板长宽必须与木板长宽对应。

  • 必须沿长边短边一刀切,每一次切割将木板分割为两个矩形。

  • 目标板的一角应与木板一角重合。

  • 为简化问题,题中所给的长宽均为整数。

    请给出木板利用率最高的切割方案。

二、问题分析与方案设计

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

动态规划状态转移方程寻找

试图寻找子问题,就寻找子问题而言,本题还是非常明显的。

每次从石板切割出一块目标块后,问题就转变为两个子问题:再从剩下的两块子块中选取切割面积最大的方案,但是,两块子块的切割方案不能重复,也不能包含刚切的目标块,这是处理的难点,这个难点体现在了动态规划的表格设计上。

动态规划的表格设计

本次问题难点在于如何设计一个合适的表格,能够恰当地表示递推关系,经过我的思考,策划,选择了许多表格后,我最终找到了以仿照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,以及以前学习的二进制枚举的知识,我想出了使用二进制作为选中标记,使用位运算完成查重的办法。

8ff23d5613335591301cd84f76c482b5.png

设计目标块使用记录的备忘录表格

目标块使用记录的备忘录表格通过开设一个与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上的测试数据。

1e36f1c9c63e62e74ba1255e64cca3d5.jpeg

测试数据输入:

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值