一、问题描述
给定一个最大的总切割目标石块,再给定一系列我们需要的样板石块。寻找切割方法使得我们从目标石块上切割出的所需样板石块的面积和最大,即对目标石块的利用率最高。限制切割为一刀切,即一次切割必须把一块石板一分为二,不能只切一段。
左边为目标石块W=8,L=4,右边为样板石块,一共四块。现在我们讨论每种样板石块只切割一块(后续通过输入可以切割k块)。所谓一刀切的意思就是,比如我先从8X4中切割4X3这个石块。第一刀我按照W=4来切,于是分成了两块4X4的石板,第二刀从其中一块石板中按L=3来切,于是分成了一块4X3和一块4X1的石块,如图。
二、算法设计
将所需的样板模块按照面积从大到小进行排序,每次取最大的那块进行切割,在数据量非常大的情况下,单步的最优利用率可能导致全局的最优利用率。进行递归切割,在递归切割的前提下进行回溯,利用限界函数来缩小时间复杂度。
回溯算法的伪代码为,
Backtrck(i)
if i > n then #到达叶节点
if cs > bests then bests = cs
else
if C(i) <= S then #走1分支
cs = cs + s[i]
Backtrck(i+1)
cs = cs - s[i]
if B(i) > bests then #不满足剪支条件可以走0分支
Backtrak(i+1)
其中C(i) = 当前要切割的样板石块的面积,S = 当前剩余可以切割的石块中最大的石块面积,如果当前要切割的面积小于剩余可以切割石块中的最大面积,说明当前石块可以被切割(走1分支)。B(i)= 当前要切割的样板石块的后面i+1,...n块样板石块的面积和,意思为,如果把剩下所有样板石块都切割完成,得到的切割总面积还小于当前已经得到的一个最好解(bests),这时就不用再走0分支,往下递归,因为再走0分支往下递归,也无法得到更好的解,这就叫满足剪支函数。反之,不满足剪支函数,也就是在当前走0分支可能得到更好解的情况下,就继续往下探索。
三、python代码实现
稍作解释:remaining_stonelist存放的是从一块完整的大石板不断切割剩下的石板块,最开始只有一块完整的大石板,每切割一次切剩下的石板就会被添加进这个列表,而原本的石板被pop出(已经被切,所以不存在)。remaining_stonelist1是为了回溯回来保存的当前剩余石板的副本。
import copy
class stone(object):
def __init__(self,width,length,s):
self.width = width
self.length = length
self.s = s
class MyGlobal():
def __init__(self):
self.cs = 0
self.bests = 0
GL = MyGlobal()
def B(GL,i,stonelist,n):
sum = 0
for a in range(i+1,n):
sum += stonelist[a].s
return sum+GL.cs
#剪支函数,如果当前面积加上把i+1..n块石板都切割完成的面积和 < 当前已经得到的bests,则不搜索0分支
def BacktrackPacking(i,stonelist,remaining_stonelist,GL,n,flag):
flag = 0
if i > n-1:
if GL.cs > GL.bests:
GL.bests = GL.cs
print("bests:", GL.bests)
else:
if stonelist[i].width < remaining_stonelist[0].width and stonelist[i].length == \
remaining_stonelist[0].length:
# 当前需要的最大石板的宽小于我要切割的石板,两者的长相等,则按照宽来一刀切
remaining_stonelist1 = copy.deepcopy(remaining_stonelist)
GL.cs += stonelist[i].s
a = stone(remaining_stonelist[0].width-stonelist[i].width, stonelist[i].length,(remaining_stonelist[0].width-stonelist[i].width)*stonelist[i].length)
remaining_stonelist.append(a)
remaining_stonelist.pop(0)
remaining_stonelist.sort(key=lambda stone:stone.s, reverse=True)
print("①:", GL.cs)
print("remaining_stonelist:有")
for k in remaining_stonelist:
print(k.width, k.length, k.s)
BacktrackPacking(i+1, stonelist, remaining_stonelist, GL, n,flag)
GL.cs -= stonelist[i].s
flag = 1
elif stonelist[i].width == remaining_stonelist[0].width and stonelist[i].length <\
remaining_stonelist[0].length:
# 当前需要的最大石板的宽等于我要切割的石板的宽,长小于我要切割的石板,则按照长来一刀切
remaining_stonelist1 = copy.deepcopy(remaining_stonelist)
GL.cs += stonelist[i].s
a = stone(stonelist[i].width, remaining_stonelist[0].length-stonelist[i].length,stonelist[i].width * (remaining_stonelist[0].length-stonelist[i].length))
remaining_stonelist.append(a)
remaining_stonelist.pop(0)
remaining_stonelist.sort(key=lambda stone: stone.s, reverse=True)
print("②:", GL.cs)
print("remaining_stonelist:有")
for k in remaining_stonelist:
print(k.width, k.length, k.s)
BacktrackPacking(i + 1, stonelist, remaining_stonelist, GL, n,flag)
GL.cs -= stonelist[i].s
flag = 1
elif stonelist[i].width == remaining_stonelist[0].width and stonelist[i].length ==\
remaining_stonelist[0].length:
# 如果当前需要的最大石板的长和宽都和我要切割的石板的长和宽相等,则匹配成功,从remaining和stonelist都去掉这两块石板
remaining_stonelist1 = copy.deepcopy(remaining_stonelist)
GL.cs += stonelist[i].s
remaining_stonelist.pop(0)
remaining_stonelist.sort(key=lambda stone: stone.s, reverse=True)
print("③:", GL.cs)
print("remaining_stonelist:有")
for k in remaining_stonelist:
print(k.width, k.length, k.s)
BacktrackPacking(i + 1, stonelist, remaining_stonelist, GL, n,flag)
GL.cs -= stonelist[i].s
flag = 1
elif stonelist[i].width < remaining_stonelist[0].width and stonelist[i].length < \
remaining_stonelist[0].length:
# 如果当前需要的最大石板的长和宽都小于我正在切割的石板的长和宽,则默认按照长来切(可以尝试默认按照宽来切,看哪种的利用率更大)
remaining_stonelist1 = copy.deepcopy(remaining_stonelist)
GL.cs += stonelist[i].s
a = stone(remaining_stonelist[0].width - stonelist[i].width, remaining_stonelist[0].length,(remaining_stonelist[0].width - stonelist[i].width) * remaining_stonelist[0].length)
remaining_stonelist.append(a)
a = stone(stonelist[i].width, remaining_stonelist[0].length-stonelist[i].length,stonelist[i].width*(remaining_stonelist[0].length-stonelist[i].length))
remaining_stonelist.append(a)
remaining_stonelist.pop(0)
remaining_stonelist.sort(key=lambda stone: stone.s, reverse=True)
print("④:", GL.cs)
print("remaining_stonelist:有")
for k in remaining_stonelist:
print(k.width, k.length, k.s)
BacktrackPacking(i + 1, stonelist, remaining_stonelist, GL, n, flag)
GL.cs -= stonelist[i].s
flag = 1
if B(GL, i, stonelist, n) > GL.bests:
print("满足剪支函数,当前GL.cs= ", GL.cs, "bests= ", GL.bests)
print("remaining_stonelist1:有")
# for k in remaining_stonelist1:
# print(k.width, k.length, k.s)
if flag == 0:
#flag=0,表示当前样板石块无法切割,直接走的0分支
BacktrackPacking(i+1, stonelist, remaining_stonelist, GL, n, flag)
elif flag == 1:
#flag=1,表示当前石板已经切割过,是回溯回来尝试走0分支,寻找更好的解
BacktrackPacking(i+1, stonelist, remaining_stonelist1, GL, n, flag)
if __name__ == '__main__':
print("请输入最大石板的宽和长:")
width = int(input())
length = int(input())
a = stone(width, length, width*length)
remaining_stonelist = []
remaining_stonelist.append(a)
stonelist = []
print("请输入样板石块的种类:")
n = int(input())
print("请输入样板石块的宽和长")
for i in range(n):
info = input().split()
b = stone(int(info[0]), int(info[1]), int(info[0])*int(info[1]))
stonelist.append(b)
stonelist.sort(key=lambda stone:stone.s, reverse=True)
# for i in stonelist:
# print(i.width,i.length,i.s)
# #对所需模块列表按照面积进行从大到小的排序
flag = 0
BacktrackPacking(0, stonelist, remaining_stonelist, GL, n, flag)
print("最大切割面积为:", GL.bests)
四、实验结果
注意,此例得到的实验结果其实不是最优解,原因是数据量过小。此例最优解应该是6+6+12=24
还有此算法可以进行优化,①每次切割都是从剩余石块列表(remaining_list)中取最大的石块切,而从最小的石块开始判断知道找到面积稍大的一块进行切割效果可能会更好。②可以寻找更好的剪支函数。