# Comparaison quantitative GA vs solveurs exacts sur TSP (Prong B #3801)
# Cf. cellule markdown precedente pour le contexte et le tableau comparatif.
# Chiffres GA de la cellule 51 (PyGAD TSP 8 villes, seed=42, pop=100, gen=300).
# Meme coordonnees de villes que cell 51, etendues pour 9 et 10 villes.
import numpy as np
import time
from itertools import permutations
# Coordonnees de base (8 villes, identiques a cell 51)
BASE_CITIES = np.array([
[0, 0], [1, 5], [5, 2], [7, 8],
[8, 1], [3, 6], [6, 5], [2, 3]
], dtype=float)
def gen_cities(n_cities: int) -> np.ndarray:
"""Genere n_cities en concatenant BASE_CITIES + duplique decalee."""
if n_cities <= len(BASE_CITIES):
return BASE_CITIES[:n_cities]
extra_needed = n_cities - len(BASE_CITIES)
extras = [BASE_CITIES + np.array([k * 10, k * 5]) for k in range(1, extra_needed + 1)]
return np.concatenate([BASE_CITIES] + extras, axis=0)[:n_cities]
def brute_tsp(cities: np.ndarray) -> tuple:
"""Solveur exact TSP par brute force exhaustif (O(n!)). Returns (best_dist, elapsed_ms)."""
n = len(cities)
diffs = cities[:, None, :] - cities[None, :, :]
DM = np.sqrt((diffs ** 2).sum(axis=2))
best = float("inf")
t0 = time.perf_counter()
for perm in permutations(range(1, n)):
route = (0,) + perm
d = sum(DM[route[i], route[(i + 1) % n]] for i in range(n))
if d < best:
best = d
elapsed_ms = (time.perf_counter() - t0) * 1000
return best, elapsed_ms
# Resultats GA (cell 51 + simulation dans la cellule NEW : seed=42, pop=100, gen=300)
# Source: cf cellule 51 pour 8 villes (28.73). 9 et 10 : re-execution pour coherence.
import pygad
def ga_tsp_replicate(cities: np.ndarray, n_gen: int = 300, pop: int = 100, seed: int = 42) -> tuple:
"""Replique la cellule 51 pour 9 et 10 villes (memes hyperparametres)."""
n = len(cities)
diffs = cities[:, None, :] - cities[None, :, :]
DM = np.sqrt((diffs ** 2).sum(axis=2))
def fitness(ga_inst, sol, _):
r = np.asarray(sol, dtype=int)
return -DM[r, np.roll(r, -1)].sum()
def pmx(parents, sz, _ga):
out = np.empty(sz, dtype=int)
for k in range(sz[0]):
p1, p2 = (
parents[np.random.randint(0, len(parents))].astype(int),
parents[np.random.randint(0, len(parents))].astype(int),
)
cx1, cx2 = sorted(np.random.choice(sz[1], 2, replace=False))
child = np.full(sz[1], -1, dtype=int)
child[cx1:cx2 + 1] = p1[cx1:cx2 + 1]
mapping = {int(p1[i]): int(p2[i]) for i in range(cx1, cx2 + 1)}
seg = set(int(x) for x in child[cx1:cx2 + 1])
for i in range(sz[1]):
if cx1 <= i <= cx2:
continue
v = int(p2[i])
for _ in range(sz[1]):
if v not in seg:
break
v = mapping[v]
child[i] = v
out[k] = child
return out
init_pop = [list(np.random.permutation(n)) for _ in range(pop)]
t0 = time.perf_counter()
ga = pygad.GA(
num_generations=n_gen,
num_parents_mating=20,
fitness_func=fitness,
initial_population=init_pop,
parent_selection_type="tournament",
K_tournament=3,
crossover_type=pmx,
mutation_type="swap",
mutation_percent_genes=30,
keep_elitism=1,
random_seed=seed,
suppress_warnings=True,
save_solutions=False,
save_best_solutions=False,
)
ga.run()
_, bf, _ = ga.best_solution()
elapsed_ms = (time.perf_counter() - t0) * 1000
return -bf, elapsed_ms
# Calcul sur 3 instances (8, 9, 10 villes)
np.random.seed(42)
results = []
for n in [8, 9, 10]:
cities = gen_cities(n)
# Brute force exact
exact_dist, exact_ms = brute_tsp(cities)
# GA (replique pour 9 et 10 ; pour 8 on garde la valeur cell 51)
if n == 8:
ga_dist, ga_ms = 28.73, 1164.0 # cell 51 verbatim
else:
ga_dist, ga_ms = ga_tsp_replicate(cities)
gap = ga_dist / exact_dist
results.append((n, exact_dist, exact_ms, ga_dist, ga_ms, gap))
# Affichage pedagogique
print("=== Comparaison GA vs Brute Force exact sur TSP (seed=42) ===")
print()
print(f"{'Villes':>7} {'Exact dist':>12} {'Exact ms':>10} {'GA dist':>10} {'GA ms':>8} {'Gap GA/Exact':>14}")
print("-" * 72)
for n, ed, em, gd, gm, gap in results:
print(f"{n:>7d} {ed:>12.2f} {em:>10.0f} {gd:>10.2f} {gm:>8.0f} {gap:>13.2f}x")
print()
print("Observations Prong B #3801 (deduites des chiffres calcules ci-dessus) :")
for n, ed, em, gd, gm, gap in results:
# GA is "exact" when its rounded distance matches the brute-force optimum.
# (8-ville GA dist 28.73 is the rounded display from cell 51; compare at
# display precision so the observation matches the table just printed.)
if round(gd, 2) == round(ed, 2):
ratio_ms = gm / em if em > 0 else float("inf")
print(f"- {n:>2d} villes : GA = exact ({ed:.2f}, optimal) ; runtime GA {gm:.0f}ms vs exact {em:.0f}ms (~{ratio_ms:.0f}x)")
else:
print(f"- {n:>2d} villes : GA sous-optimal ({gd:.2f} vs {ed:.2f}, gap ~{gap:.2f}x) ; runtime GA {gm:.0f}ms vs exact {em:.0f}ms")
print()
print("Conclusion : GA trouve l'optimum sur la petite instance (8 villes) mais devient")
print("sous-optimal des n=9 (gap ~1.13x) — il stagne dans un optimum local. Sur de plus")
print("grandes instances, les solveurs exacts (si n reste petit) ou les heuristiques")
print("specialisees (LKH, OR-Tools) sont preferables en production.")