原标题:遗传算法Python实战 009.背包问题
写在前面的话
以下部分内容,来自百度
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?
背包问题里面可以附加各种限制,比如容器的大小、容量、形状等等,我们在这里不做特别复杂的设定,仅做重量限制,也就是说可以认为你放进去的物品是可以不受形状的限制,仅受重量和容积的限制,比如你要放入的是糖或者盐这类东西。
下面是我们要放入背包中的物品:
物品名称
价值
重量(kg)
容量(L)
面粉
1680
0.265
0.41
黄油
1440
0.5
0.13
糖
1840
0.441
0.29进入算法实现部分
首先,定义个物品类,用来记录物品的各种属性:
classResource:
def__init__( self, name, value, weight, volume):
self.Name = name
self.Value = value
self.Weight = weight
self.Volume = volume
然后就可以设定物品信息和条件了:设定背包的最大承载重量为10kg,最大容量为4L
items = [Resource( " 面粉", 1680, 0.265, .41),
Resource( " 黄油", 1440, 0.5, .13),
Resource( " 糖", 1840, 0.441, .29)]
maxWeight = 10
maxVolume = 4
我们的目标是在这些限制条件下,将背包里的东西价值最大化。
我们可以想想,如何通过完成这个目标。
首先我们当然是希望价值与重量比和价值与体积比最优的物品优先——这样我们可以得到尽可能高的总价值。
当我们不能再把性价比最好的资源塞进去的时候,我们就要考虑用下一个最有价值的资源填满剩余的空间,以此类推。
一旦背包装满了,我们还需要考虑,我们是否可以用其他类型的项目组合来代替一种类型的项目,以便于最大限度的增加背包内物品的总价值。
然后定义一个用于记录当前背包中资源信息的类:
classItemQuantity:
def__init__( self, item, quantity):
self.Item = item
self.Quantity = quantity
def__eq__( self, other):
returnself.Item == other.Item andself.Quantity == other.Quantity
定义健壮性类:背包算法里面,肯定是轻、小、贵为最优,所以需要重量、容积和价值三个成员变量。在对比的时候,优先对比价值,价值相同的时候情况下,对比重量,最后对比容量。
classFitness:
def__init__( self, totalWeight, totalVolume, totalValue):
self.TotalWeight = totalWeight
self.TotalVolume = totalVolume
self.TotalValue = totalValue
def__gt__( self, other):
ifself.TotalValue != other. TotalValue:
returnself.TotalValue > other.TotalValue
ifself.TotalWeight != other. TotalWeight:
returnself.TotalWeight < other.TotalWeight
returnself.TotalVolume < other.TotalVolume
def__str__( self):
return" 重量: {:0.2f} 容积: {:0.2f} 价值: {}".format(
self.TotalWeight,
self.TotalVolume,
self.TotalValue)
健壮性验证方法,把里面所有的值都累积起来。
defget_fitness(genes):
totalWeight= 0
totalVolume= 0
totalValue= 0
foriq in genes:
count= iq.Quantity
totalWeight+= iq.Item.Weight * count
totalVolume+= iq.Item.Volume * count
totalValue+= iq.Item.Value * count
returnFitness(totalWeight, totalVolume, totalValue)
我们还需要把每个物品的数量都控制在有效的范围内:
def max_quantity( item, maxWeight, maxVolume):
returnmin(int(maxWeight / item.Weight)
ifitem.Weight > 0elsesys.maxsize,
int(maxVolume / item.Volume)
ifitem.Volume > 0elsesys.maxsize)
下面这个方法是用来创建初代基因的,在基因里面添加一个ItemQuantity,用来记录剩余的重量和体积,这样我们就不会出现超限的问题了。而在创建初代基因的时候,我们尽可能的多选取一些物品,这样先把我们的背包填满,如果这些物品效果不好,我们就对他们进行替换。
def create(items, maxWeight, maxVolume):
genes = []
remainingWeight, remainingVolume = maxWeight, maxVolume
fori inrange(random.randrange( 1, len(items))):
newGene= add(genes, items, remainingWeight, remainingVolume)
ifnewGeneis not None:
genes.append( newGene)
remainingWeight -= newGene.Quantity * newGene.Item.Weight
remainingVolume -= newGene.Quantity * newGene.Item.Volume
returngenes
上面的创建初代基因的方法里面的内部方法add,作用就是在往背包里面增加东西的时候,计算背包信息的。里面调用了上面那个max_quantity方法,来控制背包里面的剩余空间,不至于超限。在添加一个物品的时候,尽量去选择还没有被加进来,避免同一组物品太多。
defadd(genes,items,maxWeight, maxVolume):
usedItems = {iq. Itemfor iq in genes}
item= random.choice( items)
whileitemin usedItems:
item= random.choice( items)
maxQuantity = max_quantity( item,maxWeight, maxVolume)
return ItemQuantity(item,maxQuantity) ifmaxQuantity > 0elseNone
下面是进化函数:里面代码的意义,见注释:
def mutate(genes, items, maxWeight, maxVolume, window):
# 滑动窗口
window.slide
# 首先计算传入的父本基因的健壮性
fitness = get_fitness(genes)
# 计算本次进化,还可以使用的剩余重量和容积
remainingWeight = maxWeight - fitness.TotalWeight
remainingVolume = maxVolume - fitness.TotalVolume
# 用目前父本基因的数量+10 分之一的概率,来控制是否要进行移动窗口赋值
removing = len(genes) > 1andrandom.randint( 0, 10) == 0
# 如果父本基因数量大于1 ,而且随机概率90% 的话
# 从父本基因里面随机挑选一个物品,然后把重量和容积累加给背包剩余的空间
# 接下去把这个物品从基因库中移除掉
ifremoving:
index = random.randrange( 0, len(genes))
iq = genes[index]
item= iq.Item
remainingWeight += item.Weight * iq.Quantity
remainingVolume += item.Volume * iq.Quantity
del genes[index]
# 如果剩余空间还有,而且基因库中的物品数量已经等于0
# 或者基因库中的物品数量小于背包里面的物品数量,
# 并且获得百分之一的概率
# 这个变量用于控制,是否还要往包里面增加物品的。
adding = (remainingWeight > 0orremainingVolume > 0) and
( len(genes) == 0or
( len(genes) < len( items) andrandom.randint( 0, 100) == 0))
# 如果背包里面还可以塞东西,则直接把物品add 到基因组中
ifadding:
newGene = add(genes, items, remainingWeight, remainingVolume)
ifnewGene is notNone:
genes.append(newGene)
return
# 随机在基因组里面挑选一件物品,加入到背包中
index = random.randrange( 0, len(genes))
iq = genes[index]
item= iq.Item
remainingWeight += item.Weight * iq.Quantity
remainingVolume += item.Volume * iq.Quantity
# 是否要改变基因库
# 如果基因组的数量小于基因库中是数量,而且符合25% 的概率
# 从基因库里面选择更多的物品填入背包中
# 此过程主要是控制背包里面尽量所有的物品都要有,而不仅是一个类别的物品
changeItem = len(genes) < len( items) andrandom.randint( 0, 4) == 0
ifchangeItem:
itemIndex = items.index(iq.Item)
start= max( 1, itemIndex - window.Size)
stop= min( len( items) - 1, itemIndex + window.Size)
item= items[ random.randint( start, stop)]
# 计算重量,如果还没有填满,则把这个物品加入到背包里面去
# 否则把这个物品从背包里面删掉。
maxQuantity = max_quantity( item, remainingWeight, remainingVolume)
ifmaxQuantity > 0:
genes[index] = ItemQuantity( item, maxQuantity
ifwindow.Size > 1elserandom.randint( 1, maxQuantity))
else:
del genes[index]
下面是一些初始化方法和内部默认方法:
初始化终止条件(这里已经手动指定终止条件了,实际上你是可以通过代码修改来实现不同组合的,不过get_fiteness方法也要相应修改)
optimal = get_fitness(
[ItemQuantity( items[ 0], 1),
ItemQuantity( items[ 1], 14),
ItemQuantity( items[ 2], 6)])
构造几个内部方法,Python支持函数指针,可以把整个方法当成参数传递过来:
def _mutate_custom( parent, custom_mutate, get_fitness):
childGenes = parent.Genes [:]
custom _mutate( childGenes)
fitness = get _fitness( childGenes)
return Chromosome( childGenes, fitness)
进化过程:
_get_improvement方法的说明,见上一节魔术方块。
defget_best(get_fitness, targetLen, optimalFitness, geneSet, display,
custom_mutate=None, custom_create=None, maxAge=None) :
ifcustom_mutate isNone:
deffnMutate(parent):
return_mutate(parent, geneSet, get_fitness)
else:
deffnMutate(parent):
return_mutate_custom(parent, custom_mutate, get_fitness)
ifcustom_create isNone:
deffnGenerateParent:
return_generate_parent(targetLen, geneSet, get_fitness)
else:
deffnGenerateParent:
genes = custom_create
returnChromosome(genes, get_fitness(genes))
forimprovement in_get_improvement(fnMutate, fnGenerateParent, maxAge):
display(improvement)
ifnotoptimalFitness > improvement.Fitness:
returnimprovement
def_get_improvement(new_child, generate_parent, maxAge):
parent = bestParent = generate_parent
yieldbestParent
historicalFitnesses = [bestParent.Fitness]
whileTrue:
child = new_child(parent)
ifparent.Fitness > child.Fitness:
ifmaxAge isNone:
continue
parent.Age += 1
ifmaxAge > parent.Age:
continue
index = bisect_left(historicalFitnesses, child.Fitness, 0,
len(historicalFitnesses))
proportionSimilar = index / len(historicalFitnesses)
ifrandom.random < exp(-proportionSimilar):
parent = child
continue
bestParent.Age = 0
parent = bestParent
continue
ifnotchild.Fitness > parent.Fitness:
child.Age = parent.Age + 1
parent = child
continue
child.Age = 0
parent = child
ifchild.Fitness > bestParent.Fitness:
bestParent = child
yieldbestParent
historicalFitnesses.append(bestParent.Fitness)
执行测试的方法:
这方法里面利用了大量的函数指针,通过封装内部方法的传递的方式,封装和初始化了大量的默认参数,是Python中的一个特性,大家有兴趣的话,请自行了解Python的语法特征deffill_knapsack:
startTime = datetime.datetime.now
window = Window( 1,
max( 1, int(len(items) / 3)),
int(len(items) / 2))
sortedItems = sorted(items, key= lambdaitem: item.Value)
deffnDisplay(candidate):
display(candidate, startTime)
deffnGetFitness(genes):
returnget_fitness(genes)
deffnCreate:
returncreate(items, maxWeight, maxVolume)
deffnMutate(genes):
mutate(genes, sortedItems, maxWeight, maxVolume, window)
best = get_best(fnGetFitness, None, optimal, None,
fnDisplay, fnMutate, fnCreate, maxAge= 50)
执行:
fill_knapsack
输出:
下面是我的一次执行,可以看见,不管初始化的情况怎么样,最后都能逼近达到我们给出来的终止条件。13x 糖重量: 5.73容积: 3.77价值: 239200: 00: 00
10x 糖, 2x 面粉, 2x 黄油重量: 5.94容积: 3.98价值: 246400: 00: 00.002989
9x 糖, 6x 黄油重量: 6.97容积: 3.39价值: 252000: 00: 00.004021
9x 糖, 8x 黄油重量: 7.97容积: 3.65价值: 280800: 00: 00.004021
10x 糖, 8x 黄油重量: 8.41容积: 3.94价值: 299200: 00: 00.004984
12x 黄油, 7x 糖重量: 9.09容积: 3.59价值: 301600: 00: 00.004984
13x 黄油, 7x 糖重量: 9.59容积: 3.72价值: 316000: 00: 00.005983
15x 黄油, 4x 糖, 2x 面粉重量: 9.79容积: 3.93价值: 323200: 00: 00.021939
15x 黄油, 5x 糖, 1x 面粉重量: 9.97容积: 3.81价值: 324800: 00: 00.042882
15x 黄油, 5x 糖, 1x 面粉重量: 9.97容积: 3.81价值: 324800: 00: 00.178553
14x 黄油, 6x 糖, 1x 面粉重量: 9.91容积: 3.97价值: 328800: 00: 00.184537
14x 黄油, 6x 糖, 1x 面粉重量: 9.91容积: 3.97价值: 328800: 00: 00.383970
结语
背包问题,应该是我们到现在为止,处理的最复杂的算法问题,因为在背包中的物品,不但可以放入还可以拿出,还需要修改……也就是说我们的基因长度是可以变化的。
另外我们还可以增加其他的条件来增加复杂程度,比如大航海时代的航运问题:船的货物与食物、淡水和船员的配比问题:
货物运太少,亏本,运太多占用其他空间
食物不够,到不了目的地
淡水不够,雷同食物
船员数量要与货物数量相关,太少,处理不了货物,太大又要消耗更多食物淡水。这四种物质相互制约,当然还可以加入航线上哪些地方可以补给食物,可以补给淡水……等等,就更加复杂了。
责任编辑: