考虑体积重量的装箱问题(贪婪策略装箱)—— 基于遗传算法
1 装箱问题简介
经典装箱问题要求把一定数量的物品放入容量相同的箱子中,在满足每个箱子装载物品的大小之和不超过箱子容量的约束下,最小化箱子数目。装箱问题是复杂的离散组合最优化问题,属于NP完全问题。
2 场景设计
已知货物的重量和体积,在满足箱子载重和容积约束的情况下,最小化箱子使用个数,并除了最后一个箱子外,前面的箱子尽量装满。
3 遗传算法设计
3.1 算子设计
采用自然数编码、锦标赛选择、顺序交叉、基本位变异、一对一生存者竞争。
染色体的自然数编码如下图所示,采用贪婪策略进行编码。
3.2 适应度设计
Fit = 总体积/(已使用车辆容积和+最后一辆车使用的容积)*总重量/(已使用车辆载重和+最后一辆车使用的载重)
这里适应度设计的目的在于让前面先装的箱子尽量装满,也就是在最小化箱子数的大目标下,使最后一辆车尽量少装货物。由于有两个参数–体积和质量,这里简单运用乘积进行平衡,可能不是最优的,但会是比较接近最优的。
3.3 遗传算法实现
# -*- coding: utf-8 -*-
import pandas as pd
import random
def tournament_select(pops,popsize,fit,tournament_size):
new_pops = []
while len(new_pops)<len(pops):
tournament_list = random.sample(range(0,popsize),tournament_size)
tournament_fit = [fit[i] for i in tournament_list]
#转化为df方便索引
tournament_df = pd.DataFrame([tournament_list,tournament_fit]).transpose().sort_values(by=1).reset_index(drop=True)
new_pop = pops[int(tournament_df.iloc[0,0])]
new_pops.append(new_pop)
return new_pops
def crossover(popsize,parent1_pops,parent2_pops,pc):
child_pops = []
for i in range(popsize):
parent1 = parent1_pops[i]
parent2 = parent2_pops[i]
child = [-1 for i in range(len(parent1))]
if random.random() >= pc:
child = parent1.copy()#随机生成一个
random.shuffle(child)
else:
#parent1
start_pos = random.randint(0,len(parent1)-1)
end_pos = random.randint(0,len(parent1)-1)
if start_pos>end_pos:start_pos,end_pos = end_pos,start_pos
child[start_pos:end_pos+1] = parent1[start_pos:end_pos+1].copy()
# parent2 -> child
list_index = list(range(end_pos+1,len(parent2)))+list(range(0,start_pos))
j = -1
for i in list_index:
for j in range(j+1,len(parent2)):
if parent2[j] not in child:
child[i] = parent2[j]
break
child_pops.append(child)
return child_pops
def mutate(pops,pm):
pops_after_mutate = []
mutate_time = 0
for i in range(len(pops)):
pop = pops[i].copy()
if random.random() < pm:
while mutate_time<3:
mut_pos1 = random.randint(0,len(pop)-1)
mut_pos2 = random.randint(0,len(pop)-1)
if mut_pos1 != mut_pos2:pop[mut_pos1],pop[mut_pos2] = pop[mut_pos2],pop[mut_pos1]
mutate_time += 1
pops_after_mutate.append(pop)
return pops_after_mutate
def package_calFitness(cargo_df,pop,max_v,max_m):
'''
输入:cargo_df-货物信息,pop-个体,max_v-箱子容积,max_m-箱子在载重
输出:适应度-fit,boxes-解码后的个体
'''
box_num = 0#装满的箱子数
v_sum,m_sum = 0,0
v,m = 0,0
boxes,box = [],[]
for j in pop:
v_j = cargo_df[cargo_df['货物序号']==j]['体积'].iloc[0]
m_j = cargo_df[cargo_df['货物序号']==j]['重量'].iloc[0]
v += v_j
m += m_j
if (v_sum+v_j <= max_v) and (m_sum+m_j <= max_m):
box.append(j)
v_sum = v_sum+v_j
m_sum =m_sum+m_j
else:
boxes.append(sorted(box))
box_num += 1
box = []
box.append(j)
v_sum = v_j
m_sum = m_j
#最后一个箱子
boxes.append(sorted(box))
#fit=总体积/(已使用车辆容积和+最后一辆车使用容积)*总重量/(已使用车辆载重和+最后一辆车使用载重)
fit = (v/(v_sum+box_num*max_v))*(m/(m_sum+box_num*max_m))
return round(fit,4),boxes
def package_GA(cargo_df,generations,popsize,tournament_size,pc,pm,max_v,max_m):
#初始化种群
cargo_list = list(cargo_df['货物序号'])
pops = [random.sample(cargo_list,len(cargo_list)) for i in range(popsize)]#种群初始化
fit,boxes = [-1]*popsize,[-1]*popsize
for i in range(popsize):
fit[i],boxes[i] = package_calFitness(cargo_df,pops[i],max_v,max_m)
best_fit = max(fit)
best_pop = pops[fit.index(max(fit))]
best_box = boxes[fit.index(max(fit))]
if best_fit==1:return best_pop#1说明除最后一辆车都装满,已是最优解
iter = 0#迭代计数
while iter < generations:
pops1 = tournament_select(pops,popsize,fit,tournament_size)
pops2 = tournament_select(pops,popsize,fit,tournament_size)
new_pops = crossover(popsize,pops1,pops2,pc)
new_pops = mutate(new_pops,pm)
iter += 1
new_fit,new_boxes = [-1]*popsize,[-1]*popsize#初始化
for i in range(popsize):
new_fit[i],new_boxes[i] = package_calFitness(cargo_df,new_pops[i],max_v,max_m)#计算适应度
for i in range(len(pops)):
if fit[i] < new_fit[i]:
pops[i] = new_pops[i]
fit[i] = new_fit[i]
boxes[i] = new_boxes[i]
if best_fit < max(fit):#保留历史最优
best_fit = max(fit)
best_pop= pops[fit.index(max(fit))]
best_box = boxes[fit.index(max(fit))]
print("第",iter,"代适应度最优值:",best_fit)
return best_pop,best_fit,best_box
if __name__ == '__main__':
#数据
num = list(range(100))#货物编号
volumns = [1,6,7,8,1,2,3,1,8,8,10,1,9,3,4,3,5,7,4,6,5,5,9,5,6,3,9,9,6,3,4,2,1,3,5,9,6,6,8,5,6,2,7,9,5,1,7,5,10,6,
4,6,9,7,2,4,3,7,5,4,5,10,2,1,4,10,9,6,10,10,10,2,10,2,4,6,4,1,7,6,1,10,1,3,4,1,7,3,6,5,3,10,6,8,1,6,4,4,10,3]#体积
weight = [3,5,3,8,10,4,7,2,10,1,9,2,1,9,7,1,7,1,4,2,5,9,1,6,1,4,2,1,2,1,5,5,6,8,3,6,7,4,9,7,7,4,8,3,9,4,1,1,9,5,8,
4,10,3,5,1,7,8,8,2,8,7,1,10,3,3,8,2,4,6,8,3,5,8,10,5,7,5,7,1,9,1,5,9,9,2,10,2,9,3,7,10,5,1,2,1,9,8,6,9]#重量
cargo_df = pd.DataFrame({'货物序号':num, "体积":volumns, "重量":weight})
M,V= 100,100 #箱子载重容积
#GA参数
generations = 50
popsize = 40
tournament_size = 4
pc = 0.9
pm = 0.1
pop,fit,box = package_GA(cargo_df,generations,popsize,tournament_size,pc,pm,V,M)
print("最优解:",box)
3.4 结果
最优解: [[0, 1, 6, 9, 14, 17, 21, 23, 24, 25, 31, 51, 52, 53, 57, 59, 82, 88, 98], [2, 5, 10, 12, 26, 34, 37, 38, 40, 54, 55, 64, 77, 78, 83, 87, 90, 94, 96, 97], [18, 19, 28, 43, 45, 47, 50, 56, 58, 60, 63, 67, 70, 71, 76, 79, 80, 81, 93, 99], [11, 13, 16, 29, 30, 32, 33, 35, 36, 39, 41, 46, 48, 49, 69, 75, 91, 95], [3, 4, 7, 8, 20, 22, 27, 42, 44, 62, 66, 68, 72, 73, 74, 85, 92], [15, 61, 65, 84, 86, 89]]
,总共需要6个箱子,箱子货物体积分别是[100, 98, 99, 97, 97, 39],重量分别是[96, 100, 98, 99, 97, 33]。
4 总结
装箱问题的目标是最小化箱子使用数量,在实际应用中,可能会有更细化的一个目标,比如先装货物的箱子尽量装满(最后一个箱子尽量少装),或者平衡所有箱子的载重和容积,这里实现的差别主要在于编码和适应度设计。本文求解的问题是尽量将前面先装的箱子装满,使用遗传算法进行求解,求解速度还是有点慢,不过从适应度上看求解效果应该还是不错的。
记录学习过程,欢迎指正