import random
class Space():
def __init__(self, height, width, num_hospitals):
"""Create a new state space with given dimensions."""
self.height = height
self.width = width
self.num_hospitals = num_hospitals
self.houses = set()
self.hospitals = set()
def add_house(self, row, col):
"""Add a house at a particular location in state space."""
self.houses.add((row, col))
def available_spaces(self):
"""Returns all cells not currently used by a house or hospital."""
# Consider all possible cells
candidates = set(
(row, col)
for row in range(self.height)
for col in range(self.width)
)
# Remove all houses and hospitals
for house in self.houses:
candidates.remove(house)
for hospital in self.hospitals:
candidates.remove(hospital)
return candidates
def hill_climb(self, maximum=None, image_prefix=None, log=False):
"""Performs hill-climbing to find a solution."""
count = 0
# Start by initializing hospitals randomly
self.hospitals = set()
for i in range(self.num_hospitals):
self.hospitals.add(random.choice(list(self.available_spaces())))
if log:
print("Initial state: cost", self.get_cost(self.hospitals))
if image_prefix:
self.output_image(f"{image_prefix}{str(count).zfill(3)}.png")
# Continue until we reach maximum number of iterations
while maximum is None or count < maximum:
count += 1
best_neighbors = []
best_neighbor_cost = None
# Consider all hospitals to move
for hospital in self.hospitals:
# Consider all neighbors for that hospital
for replacement in self.get_neighbors(*hospital):
# Generate a neighboring set of hospitals
neighbor = self.hospitals.copy()
neighbor.remove(hospital)
neighbor.add(replacement)
# Check if neighbor is best so far
cost = self.get_cost(neighbor)
if best_neighbor_cost is None or cost < best_neighbor_cost:
best_neighbor_cost = cost
best_neighbors = [neighbor]
elif best_neighbor_cost == cost:
best_neighbors.append(neighbor)
# None of the neighbors are better than the current state
if best_neighbor_cost >= self.get_cost(self.hospitals):
return self.hospitals
# Move to a highest-valued neighbor
else:
if log:
print(f"Found better neighbor: cost {best_neighbor_cost}")
self.hospitals = random.choice(best_neighbors)
# Generate image
if image_prefix:
self.output_image(f"{image_prefix}{str(count).zfill(3)}.png")
def random_restart(self, maximum, image_prefix=None, log=False):
"""Repeats hill-climbing multiple times."""
best_hospitals = None
best_cost = None
# Repeat hill-climbing a fixed number of times
for i in range(maximum):
hospitals = self.hill_climb()
cost = self.get_cost(hospitals)
if best_cost is None or cost < best_cost:
best_cost = cost
best_hospitals = hospitals
if log:
print(f"{i}: Found new best state: cost {cost}")
else:
if log:
print(f"{i}: Found state: cost {cost}")
if image_prefix:
self.output_image(f"{image_prefix}{str(i).zfill(3)}.png")
return best_hospitals
def get_cost(self, hospitals):
"""Calculates sum of distances from houses to nearest hospital."""
cost = 0
for house in self.houses:
cost += min(
abs(house[0] - hospital[0]) + abs(house[1] - hospital[1])
for hospital in hospitals
)
return cost
def get_neighbors(self, row, col):
"""Returns neighbors not already containing a house or hospital."""
candidates = [
(row - 1, col),
(row + 1, col),
(row, col - 1),
(row, col + 1)
]
neighbors = []
for r, c in candidates:
if (r, c) in self.houses or (r, c) in self.hospitals:
continue
if 0 <= r < self.height and 0 <= c < self.width:
neighbors.append((r, c))
return neighbors
def output_image(self, filename):
"""Generates image with all houses and hospitals."""
from PIL import Image, ImageDraw, ImageFont
cell_size = 100
cell_border = 2
cost_size = 40
padding = 10
# Create a blank canvas
img = Image.new(
"RGBA",
(self.width * cell_size,
self.height * cell_size + cost_size + padding * 2),
"white"
)
house = Image.open("assets/images/House.png").resize(
(cell_size, cell_size)
)
hospital = Image.open("assets/images/Hospital.png").resize(
(cell_size, cell_size)
)
f 30)
draw = ImageDraw.Draw(img)
for i in range(self.height):
for j in range(self.width):
# Draw cell
rect = [
(j * cell_size + cell_border,
i * cell_size + cell_border),
((j + 1) * cell_size - cell_border,
(i + 1) * cell_size - cell_border)
]
draw.rectangle(rect, fill="black")
if (i, j) in self.houses:
img.paste(house, rect[0], house)
if (i, j) in self.hospitals:
img.paste(hospital, rect[0], hospital)
# Add cost
draw.rectangle(
(0, self.height * cell_size, self.width * cell_size,
self.height * cell_size + cost_size + padding * 2),
"black"
)
draw.text(
(padding, self.height * cell_size + padding),
f"Cost: {self.get_cost(self.hospitals)}",
fill="white",
f
)
img.save(filename)
# Create a new space and add houses randomly
s = Space(height=10, width=20, num_hospitals=3)
for i in range(15):
s.add_house(random.randrange(s.height), random.randrange(s.width))
# Use local search to determine hospital placement
hospitals = s.hill_climb(image_prefix="hospitals", log=True)