162 lines
4.8 KiB
Python
162 lines
4.8 KiB
Python
import math
|
|
import random
|
|
from dataclasses import dataclass
|
|
from typing import List, Sequence, Tuple
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
City = Tuple[float, float]
|
|
Tour = List[int]
|
|
|
|
|
|
def euclidean_distance(c1: City, c2: City) -> float:
|
|
return math.hypot(c1[0] - c2[0], c1[1] - c2[1])
|
|
|
|
|
|
def build_distance_matrix(cities: Sequence[City]) -> list[list[float]]:
|
|
size = len(cities)
|
|
matrix = [[0.0 for _ in range(size)] for _ in range(size)]
|
|
for i in range(size):
|
|
for j in range(i + 1, size):
|
|
dist = euclidean_distance(cities[i], cities[j])
|
|
matrix[i][j] = matrix[j][i] = dist
|
|
return matrix
|
|
|
|
|
|
def plot_tour(cities: Sequence[City], tour: Sequence[int], save_path: str) -> None:
|
|
ordered = [cities[i] for i in tour] + [cities[tour[0]]]
|
|
xs, ys = zip(*ordered)
|
|
|
|
fig, ax = plt.subplots(figsize=(7, 7))
|
|
ax.plot(xs, ys, "-o", color="#1f77b4", markersize=4, linewidth=1.5)
|
|
city_xs, city_ys = zip(*cities)
|
|
ax.scatter(city_xs, city_ys, s=18, color="#d62728", zorder=5)
|
|
|
|
ax.set_xlabel("X")
|
|
ax.set_ylabel("Y")
|
|
ax.set_title("Маршрут тура")
|
|
ax.set_aspect("equal", adjustable="box")
|
|
ax.grid(True, linestyle="--", alpha=0.3)
|
|
fig.tight_layout()
|
|
fig.savefig(save_path, dpi=220)
|
|
plt.close(fig)
|
|
|
|
|
|
def plot_history(best_lengths: Sequence[float], save_path: str) -> None:
|
|
if not best_lengths:
|
|
return
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 3.8))
|
|
ax.plot(best_lengths, color="#111111", linewidth=1.4)
|
|
ax.set_xlabel("Итерация")
|
|
ax.set_ylabel("Длина лучшего тура")
|
|
ax.set_title("Сходимость ACO")
|
|
ax.grid(True, linestyle="--", alpha=0.4)
|
|
fig.tight_layout()
|
|
fig.savefig(save_path, dpi=220)
|
|
plt.close(fig)
|
|
|
|
|
|
@dataclass
|
|
class ACOConfig:
|
|
cities: Sequence[City]
|
|
n_ants: int
|
|
n_iterations: int
|
|
alpha: float = 1.0
|
|
beta: float = 5.0
|
|
rho: float = 0.5
|
|
q: float = 1.0
|
|
seed: int | None = None
|
|
|
|
|
|
@dataclass
|
|
class ACOResult:
|
|
best_tour: Tour
|
|
best_length: float
|
|
history: List[float]
|
|
|
|
|
|
class AntColonyOptimizer:
|
|
def __init__(self, config: ACOConfig):
|
|
self.config = config
|
|
if config.seed is not None:
|
|
random.seed(config.seed)
|
|
|
|
self.cities = config.cities
|
|
self.dist_matrix = build_distance_matrix(config.cities)
|
|
n = len(config.cities)
|
|
self.pheromone = [[1.0 if i != j else 0.0 for j in range(n)] for i in range(n)]
|
|
|
|
def _choose_next_city(self, current: int, unvisited: set[int]) -> int:
|
|
candidates = list(unvisited)
|
|
weights = []
|
|
for nxt in candidates:
|
|
tau = self.pheromone[current][nxt] ** self.config.alpha
|
|
eta = (1.0 / (self.dist_matrix[current][nxt] + 1e-12)) ** self.config.beta
|
|
weights.append(tau * eta)
|
|
|
|
total = sum(weights)
|
|
probs = [w / total for w in weights]
|
|
return random.choices(candidates, weights=probs, k=1)[0]
|
|
|
|
def _build_tour(self, start: int) -> Tour:
|
|
n = len(self.cities)
|
|
tour = [start]
|
|
unvisited = set(range(n))
|
|
unvisited.remove(start)
|
|
|
|
current = start
|
|
while unvisited:
|
|
nxt = self._choose_next_city(current, unvisited)
|
|
tour.append(nxt)
|
|
unvisited.remove(nxt)
|
|
current = nxt
|
|
|
|
return tour
|
|
|
|
def _tour_length(self, tour: Sequence[int]) -> float:
|
|
return sum(
|
|
self.dist_matrix[tour[i]][tour[(i + 1) % len(tour)]]
|
|
for i in range(len(tour))
|
|
)
|
|
|
|
def run(self) -> ACOResult:
|
|
best_tour: Tour = []
|
|
best_length = float("inf")
|
|
best_history: list[float] = []
|
|
|
|
for _ in range(self.config.n_iterations):
|
|
tours: list[Tour] = []
|
|
lengths: list[float] = []
|
|
|
|
for _ in range(self.config.n_ants):
|
|
start_city = random.randrange(len(self.cities))
|
|
tour = self._build_tour(start_city)
|
|
length = self._tour_length(tour)
|
|
tours.append(tour)
|
|
lengths.append(length)
|
|
|
|
if length < best_length:
|
|
best_length = length
|
|
best_tour = tour
|
|
|
|
for i in range(len(self.pheromone)):
|
|
for j in range(len(self.pheromone)):
|
|
self.pheromone[i][j] *= 1 - self.config.rho
|
|
|
|
for tour, length in zip(tours, lengths):
|
|
deposit = self.config.q / length
|
|
for i in range(len(tour)):
|
|
a, b = tour[i], tour[(i + 1) % len(tour)]
|
|
self.pheromone[a][b] += deposit
|
|
self.pheromone[b][a] += deposit
|
|
|
|
best_history.append(best_length)
|
|
|
|
return ACOResult(best_tour=best_tour, best_length=best_length, history=best_history)
|
|
|
|
|
|
def run_aco(config: ACOConfig) -> ACOResult:
|
|
optimizer = AntColonyOptimizer(config)
|
|
return optimizer.run()
|