Knights Problem
Knights Problem 要求使用最小数量的 knight 来攻击整个棋盘上的方块,因此棋盘至少是 3 * 4 的才能满足情况。knight能够攻击的范围如下所示。
其 genetic.py 的代码为:
import random
import statistics
import sys
import time
def _generate_parent(length, geneSet, get_fitness):
genes = []
while len(genes) < length:
sampleSize = min(length - len(genes), len(geneSet))
genes.extend(random.sample(geneSet, sampleSize))
fitness = get_fitness(genes)
return Chromosome(genes, fitness)
def _mutate(parent, geneSet, get_fitness):
childGenes = parent.Genes[:]
index = random.randrange(0, len(parent.Genes))
newGene, alternate = random.sample(geneSet, 2)
childGenes[index] = alternate if newGene == childGenes[index] else newGene
fitness = get_fitness(childGenes)
return Chromosome(childGenes, fitness)
def _mutate_custom(parent, custom_mutate, get_fitness):
childGenes = parent.Genes[:]
custom_mutate(childGenes)
fitness = get_fitness(childGenes)
return Chromosome(childGenes, fitness)
def get_best(get_fitness, targetLen, optimalFitness, geneSet, display,
custom_mutate=None, custom_create=None):
if custom_mutate is None:
def fnMutate(parent):
return _mutate(parent, geneSet, get_fitness)
else:
def fnMutate(parent):
return _mutate_custom(parent, custom_mutate, get_fitness)
if custom_create is None:
def fnGenerateParent():
return _generate_parent(targetLen, geneSet, get_fitness)
else:
def fnGenerateParent():
genes = custom_create()
return Chromosome(genes, get_fitness(genes))
for improvement in _get_improvement(fnMutate, fnGenerateParent):
display(improvement)
if not optimalFitness > improvement.Fitness:
return improvement
def _get_improvement(new_child, generate_parent):
bestParent = generate_parent()
yield bestParent
while True:
child = new_child(bestParent)
if bestParent.Fitness > child.Fitness:
continue
if not child.Fitness > bestParent.Fitness:
bestParent = child
continue
yield child
bestParent = child
class Chromosome:
def __init__(self, genes, fitness):
self.Genes = genes
self.Fitness = fitness
class Benchmark:
@staticmethod
def run(function):
timings = []
stdout = sys.stdout
for i in range(100):
sys.stdout = None
startTime = time.time()
function()
seconds = time.time() - startTime
sys.stdout = stdout
timings.append(seconds)
mean = statistics.mean(timings)
if i < 10 or i % 10 == 9:
print("{} {:3.2f} {:3.2f}".format(
1 + i, mean,
statistics.stdev(timings, mean) if i > 1 else 0))
上面的代码中函数 get_best 增加了一个参数 custom_create ,目的是利用自定义的创造函数产生父染色体。
KnightsTest.py 的完整代码为:
import datetime
import random
import unittest
import genetic
def get_fitness(genes, boardWidth, boardHeight):
attacked = set(pos
for kn in genes
for pos in get_attacks(kn, boardWidth, boardHeight))
return len(attacked)
def display(candidate, startTime, boardWidth, boardHeight):
timeDiff = datetime.datetime.now() - startTime
board = Board(candidate.Genes, boardWidth, boardHeight)
board.print()
print("{}
{} {}".format(
' '.join(map(str, candidate.Genes)),
candidate.Fitness,
timeDiff))
def mutate(genes, boardWidth, boardHeight, allPositions, nonEdgePositions):
count = 2 if random.randint(0, 10) == 0 else 1
while count > 0:
count -= 1
positionToKnightIndexes = dict((p, []) for p in allPositions)
for i, knight in enumerate(genes):
for position in get_attacks(knight, boardWidth, boardHeight):
positionToKnightIndexes[position].append(i)
knightIndexes = set(i for i in range(len(genes)))
unattacked = []
for kvp in positionToKnightIndexes.items():
if len(kvp[1]) > 1:
continue
if len(kvp[1]) == 0:
unattacked.append(kvp[0])
continue
for p in kvp[1]: # len == 1
if p in knightIndexes:
knightIndexes.remove(p)
potentialKnightPositions =
[p for positions in
map(lambda x: get_attacks(x, boardWidth, boardHeight),
unattacked)
for p in positions if p in nonEdgePositions]
if len(unattacked) > 0 else nonEdgePositions
geneIndex = random.randrange(0, len(genes))
if len(knightIndexes) == 0
else random.choice([i for i in knightIndexes])
position = random.choice(potentialKnightPositions)
genes[geneIndex] = position
def create(fnGetRandomPosition, expectedKnights):
genes = [fnGetRandomPosition() for _ in range(expectedKnights)]
return genes
def get_attacks(location, boardWidth, boardHeight):
return [i for i in set(
Position(x + location.X, y + location.Y)
for x in [-2, -1, 1, 2] if 0 <= x + location.X < boardWidth
for y in [-2, -1, 1, 2] if 0 <= y + location.Y < boardHeight
and abs(y) != abs(x))]
class KnightsTests(unittest.TestCase):
def test_3x4(self):
width = 4
height = 3
# 1,0 2,0 3,0
# 0,2 1,2 2,2
# 2 N N N .
# 1 . . . .
# 0 . N N N
# 0 1 2 3
self.find_knight_positions(width, height, 6)
def test_8x8(self):
width = 8
height = 8
self.find_knight_positions(width, height, 14)
def test_10x10(self):
width = 10
height = 10
self.find_knight_positions(width, height, 22)
def test_12x12(self):
width = 12
height = 12
self.find_knight_positions(width, height, 28)
def test_13x13(self):
width = 13
height = 13
self.find_knight_positions(width, height, 32)
def test_benchmark(self):
genetic.Benchmark.run(lambda: self.test_10x10())
def find_knight_positions(self, boardWidth, boardHeight, expectedKnights):
startTime = datetime.datetime.now()
def fnDisplay(candidate):
display(candidate, startTime, boardWidth, boardHeight)
def fnGetFitness(genes):
return get_fitness(genes, boardWidth, boardHeight)
allPositions = [Position(x, y)
for y in range(boardHeight)
for x in range(boardWidth)]
if boardWidth < 6 or boardHeight < 6:
nonEdgePositions = allPositions
else:
nonEdgePositions = [i for i in allPositions
if 0 < i.X < boardWidth - 1 and
0 < i.Y < boardHeight - 1]
def fnGetRandomPosition():
return random.choice(nonEdgePositions)
def fnMutate(genes):
mutate(genes, boardWidth, boardHeight, allPositions,
nonEdgePositions)
def fnCreate():
return create(fnGetRandomPosition, expectedKnights)
optimalFitness = boardWidth * boardHeight
best = genetic.get_best(fnGetFitness, None, optimalFitness, None,
fnDisplay, fnMutate, fnCreate)
self.assertTrue(not optimalFitness > best.Fitness)
class Position:
def __init__(self, x, y):
self.X = x
self.Y = y
def __str__(self):
return "{},{}".format(self.X, self.Y)
def __eq__(self, other):
return self.X == other.X and self.Y == other.Y
def __hash__(self):
return self.X * 1000 + self.Y
class Board:
def __init__(self, positions, width, height):
board = [['.'] * width for _ in range(height)]
for index in range(len(positions)):
knightPosition = positions[index]
board[knightPosition.Y][knightPosition.X] = 'N'
self._board = board
self._width = width
self._height = height
def print(self):
# 0,0 prints in bottom left corner
for i in reversed(range(self._height)):
print(i, " ", ' '.join(self._board[i]))
print(" ", ' '.join(map(str, range(self._width))))
if __name__ == '__main__':
unittest.main()
其中计算适应值的函数如下,适应值由棋盘上被攻击的方块数决定。
def get_fitness(genes, boardWidth, boardHeight):
attacked = set(pos
for kn in genes
for pos in get_attacks(kn, boardWidth, boardHeight))
return len(attacked)
为了缩小求解空间,我们自定义了函数 mutate。
def mutate(genes, boardWidth, boardHeight, allPositions, nonEdgePositions):
count = 2 if random.randint(0, 10) == 0 else 1
while count > 0:
count -= 1
positionToKnightIndexes = dict((p, []) for p in allPositions)
for i, knight in enumerate(genes):
for position in get_attacks(knight, boardWidth, boardHeight):
positionToKnightIndexes[position].append(i)
knightIndexes = set(i for i in range(len(genes)))
unattacked = []
for kvp in positionToKnightIndexes.items():
if len(kvp[1]) > 1:
continue
if len(kvp[1]) == 0:
unattacked.append(kvp[0])
continue
for p in kvp[1]: # len == 1
if p in knightIndexes:
knightIndexes.remove(p)
potentialKnightPositions =
[p for positions in
map(lambda x: get_attacks(x, boardWidth, boardHeight),
unattacked)
for p in positions if p in nonEdgePositions]
if len(unattacked) > 0 else nonEdgePositions
geneIndex = random.randrange(0, len(genes))
if len(knightIndexes) == 0
else random.choice([i for i in knightIndexes])
position = random.choice(potentialKnightPositions)
genes[geneIndex] = position
首先,有十分之一的可能性突变两次,防止陷入局部最优解。
在突变之前,先统计所有被攻击的方块并保存在键值对 positionToKnightIndexes 中,键为位置,值为攻击这个位置的所有骑士的列表。
接着将所有的未被攻击的位置存储在 unattacked 中,所有已经攻击其他方块一次的knight的序号从 knightIndexs 中删除。
如果未被攻击的方块数>0,则 potentialKnightPositions 为能够攻击untackked方块的并且还是非边界的位置。否则 potentialKnightPositions 为非边界的位置。
如果 knightIndexes 长度不为0,则随机选择其中一个进行突变。如果为0,则在genes长度中选择一个索引进行突变。