第二次实验报告
程序语言: python
姓名: unicorn
学号: 12345678910
日期:2023/3/24
一、 问题重述
给定一块长为L,宽为W的石板,现在需要从石板上分别切割出n个长度为li,宽度为wi的石砖。切割的规则是石砖的长度方向与石板的长度方向保持一致,同时满足一刀切的约束(即刀无法拐弯,无法中途停下)。问如何切割使得所使用的石材利用率最高?
其他要求:递归算法。任给一个输入实例,能输出切割所需要实际高度,能用图形演示切割的过程。
二、 问题分析
这里先考虑理想的情况即给定一个长宽限定的矩形中石板,对石板进行切割。首先要选用哪一个石砖先进行切割,根据经验判断,认为最大的石砖先进行切割可以使利用率尽可能最高。所以可以考虑先将所需要切割的石板按照面积大小进行排序,有三种特殊情况要优先处理,第一是石砖长宽与石板重合,此时石板无需切割是最好的情况,优先判断是否存在该种情形,第二,石砖长宽中存在一边与石板重合,此时也应该优先处理,因为切割完只剩下一块石板,减少递归的工作量。然后判断石板要从哪个方向切割,因为从长度方向和宽度方向切割是基本等效的,那么我们考虑生成一个更大的剩余面积,即切割后剩下的两块石板一块面积尽可能大,一块尽可能小,让较多的石砖能够在尽可能大的石板中继续切割。接着考虑先对哪一块石板进行递归,按照经验判断,认为应先对面积更小的石板进行切割。最后根据累计的面积计算利用率
数据结构:用Class类来实现一个在C/C++中呈现的结构体;用列表实现结构体数组
三、 伪代码
切割函数伪代码如下:
def cut(x, y, l, w):
for i<-0 to num do
if not lis[i].visited then
wi <- lis[i].width
li <- lis[i].length
if li > l or wi > w then
continue
elif wi is w then
lis[i].visited <- True
qiege(x, y, l, w, i)
if li is l then
return
else
cut(x, y + li, l - li, w)
return
elif li is l then
lis[i].visited <- True
qiege(x, y, l, w, i)
cut(x + wi, y, l, w - wi)
return
for i<-0 to num do
if not lis[i].visited then
wi <- lis[i].width
li <- lis[i].length
if li > l or wi > w then
continue
else
lis[i].visited = True
qiege(x, y, l, w, i)
if (w - wi) * li <= wi * (l - li) or Benchmark then
if (w - wi) * li <= (l - li) * w then
cut(x + wi, y, li, w - wi)
cut(x, y + li, l - li, w)
break
else
cut(x, y + li, l - li, w)
cut(x + wi, y, li, w - wi)
break
else
if (l - li) * wi <= li * (w - wi) then
cut(x + wi, y, l, w - wi)
cut(x, y + li, l - li, wi)
break
else
cut(x, y + li, l - li, wi)
cut(x + wi, y, l, w - wi)
break
return
四、代码
代码如下:
from typing import List, Any
import turtle
import time
Benchmark = 1 # 默认为benchmark数据初始量为1
number = 0
# 放大倍率:为了让turtle看得更清楚而进行放大
N = 1
# 基准,将石板左小角往左下角移动
x_datum = 300
y_datum = 200
Length = 10000 # 无数据默认10000
class stone: # 结构体
def __init__(self):
self.length = 0
self.width = 0
self.area = 0
self.visited = False
# 矩形
def rectangle(x, y, l, w, k):
x *= N
x-=x_datum
y *= N
y -= y_datum
l *= N
w *= N
idc = turtle.Turtle()
if k == 0:
idc.color('black', 'blue')
else:
idc.color('black', 'green')
idc.up()
idc.hideturtle()
idc.goto(x, y)
idc.down()
idc.begin_fill()
idc.forward(w)
idc.left(90) # 逆时针旋转90度
idc.forward(l)
idc.left(90)
idc.forward(w)
idc.left(90)
idc.forward(l)
idc.left(90)
idc.end_fill()
def qiege(x, y, l, w, i):
global max_len,number
number+=1
if max_len < y + lis[i].length:
max_len = y + lis[i].length
print("第"+str(number)+"次切割石板" + str(i) + "石板宽为" +
str(lis[i].width) + "石板长为" + str(lis[i].length) + "此时" +
str(x) + " " + str(y) + " " + str(l) + " " + str(w))
rectangle(x, y, l, w, 0)
rectangle(x, y, lis[i].length, lis[i].width, 1)
def swapPositions(listt, pos1, pos2):
listt[pos1], listt[pos2] = listt[pos2], listt[pos1]
return listt
def cut(x, y, l, w):
# x,y是当前石板左下角的横纵坐标 ;w,l是当前石板的长度和宽度
# 先找长宽一致的
global max_len
print(x, y, l, w)
for i in range(0, num):
if not lis[i].visited:
wi = lis[i].width
li = lis[i].length
if li > l or wi > w:
continue
elif lis[i].width is w:
lis[i].visited = True
qiege(x, y, l, w, i)
if lis[i].length is l: # 完全符合直接退出,修改相关数据
return
else: # 只有宽符合,长度不符合
cut(x, y + lis[i].length, l - lis[i].length, w) # 只返回一个
return
elif lis[i].length is l: # 只有长符合,宽不符合
lis[i].visited = True
qiege(x, y, l, w, i)
cut(x + lis[i].width, y, l, w - lis[i].width) # 只返回一个
return
for i in range(0, num):
# 进入这个循环说明前面已经没有前面没有一边刚好的,都不符合,先找剩下面积小的是哪一个
if not lis[i].visited:
wi = lis[i].width
li = lis[i].length
if li > l or wi > w:
continue
else:
lis[i].visited = True
qiege(x, y, l, w, i)
if (w - wi) * li <= wi * (l - li) or Benchmark:
# 横切使得较小面积较小,先横切 或者是 高度尽可能低必须横切
if (w - wi) * li <= (l - li) * w: # 判断面积选择较小的石板先进性递归
cut(x + wi, y, li, w - wi)
cut(x, y + li, l - li, w)
break
else:
cut(x, y + li, l - li, w)
cut(x + wi, y, li, w - wi)
break
else: # 纵切使得面积小
if (l - li) * wi <= li * (w - wi): # 判断面积选择较小的石板先进性递归
cut(x + wi, y, l, w - wi)
cut(x, y + li, l - li, wi)
break
else:
cut(x, y + li, l - li, wi)
cut(x + wi, y, l, w - wi)
break
return
if Benchmark == 1:
ifLength = 0 # 常量用来判断是否输入长度
lis = [] # 石头数据的结构体数组
total_area = 0
max_len = 0
num = int(input())
if ifLength:
Length = int(input())
Width = int(input())
for i in range(0, num):
lis.append(stone()) # 添加一个结构体
array = []
string = input()
array = string.split(' ')
n = int(array[0])
if i != n:
print("数据出错,请检查")
break
lis[i].width = int(array[1])
lis[i].length = int(array[2])
lis[i].area = lis[i].length * lis[i].width
# 进行数据预处理 简单的冒泡排序
for i in range(0, num - 1):
for j in range(0, num - 1 - i):
if lis[j].area < lis[j + 1].area:
swapPositions(lis, j, j + 1)
# cut(0,0,Length,Width)
turtle.setup(width=1000, height=800)
# 颜色
turtle.color('red', 'pink')
# 笔粗细
turtle.pensize(1)
# 速度
turtle.speed(0)
turtle.delay(0)
turtle.hideturtle()
cut(0, 0, Length, Width)
total = 0
for i in range(0, num):
if lis[i].visited:
total_area += lis[i].area
total += lis[i].area
availability = total_area / (Length * Width) * 100
print("面积为" + str(total_area) + " 石砖总面积为" + str(total) +
" 石板总面积为" + str(Length * Width))
print("利用率为" + str(availability) + "%")
print("最大高度为" + str(max_len))
# 点击窗口关闭
window = turtle.Screen()
window.exitonclick()
五、 实验结果
输出大概如图
turtle可视化如下图:
切图过程(节选)
石板切割问题可视化
六、 遇到的一些问题
第一、可以看到我在每次切割后面都加了return或者break(第一个循环必须return第二个循环可以用return或者break)。之前没有考虑到就没有加,于是就会导致第一个石砖切完循环到第二个石砖如果也满足就会继续切第二个石砖,这时候的x,y和l,w都是切第一个石砖的,但是已经切完第一个石砖了,这些数值应该会变化。换句话说就是每个(x,y,l,w)切一次石砖,所以找到可以切的就直接返回,因为完成任务了。所以后面就增加了return或者break
第二、这个其实是第二版,检查代码的时候发现只有一个循环会导致与思路不符,于是改成两个循环(具体看总结,感觉放在总结比较合适,因为这个跟思路实现有关)
第三、本来的cut里面还有count计数来算如果每一个石砖都切过了就可以退出了。但是其实多此一举,因为如果一次循环的话本来就要遍历一次才能判断遍历完没有也是直接返回你判断还多一句。但是两个循环就可以设置一下,可以少走一个循环。
第四、因为我也是第一次接触turtle来画图,还好这个比较好上手。就是如果数据太大太小可能会看不见或者超过框。我设置一个N来调整放大和缩小的倍率,还有x_datum和y_datum来调整坐标原点。大家可以根据自己的数据情况进行调整来方便观察切割情况。
七、 总结
还是一样存在两个问题:
第一、因为老师给的基准数据是只有石板的宽和小石砖长宽数据,并没有石板的长。但是我这里的代码考虑的是尽量满足两种情况的最简。如果只考虑老师给的数据不给长度(或者叫高度?我这里统称长度吧)计算长度最小值的类型的话,石砖选用的优先顺序我想应该是长宽都符合的=>只有一边符合的=>长度更长。但这里是只优先面积没有优先长度可能会造成长度值不是最理想的。
第二、由于整个程序使用分支界限的递归算法,但是这个递归只会考虑一种情况,不会考虑其他情况,算法只会根据我们给定的尽量优解的切割思路进行从上到小的一次递归然后就会直接返回。如果能够在一次递归后返回上层递归进行另一种可能性的尝试并进行最小值的比较,又可能会得到更优的结果。但是考虑到这个算法基本已经算是可以直接找到较优秀的切割方法,如果要实现的话可能花费一些较长的没用的时间。且需要实现最终结果的画图还需要储存每次最优解的结果。所以选择上述代码实现。
上述代码其实是我第二版的代码。第一版的代码在将代码实现按面积大小排序后在切割函数时直接依次遍历没使用过的函数,小于等于切割石板且不符合的直接开始切,这样会导致符合优先的思想无法实现,因为可能下一个石板就是符合的,但是前一个不符合还没轮到这个符合的石板就直接切了。所以就修改成上述代码。专门设立一个循环寻找符合的石砖,等完全遍历一遍后再来循环一编,此时都是不符合的,再从不符合的代码中依次切割,这时候就可以遵循“长宽都符合的=>只有一边符合的=>面积更大”的思想。经过实验验证。前者(旧版第一版)在应对数据较小或者给定长度较小时较优秀(因为面积最大优先),而后者在应对数据较大或者给定长度较长或无限长情况下更优秀。
如果需要旧版代码或者老师给的测试数据可以私聊笔者
切割石板回溯算法