TSP: Ewolucja Drogi (Algorytm Genetyczny)

Dlaczego stosujemy ewolucję? Problem Komiwojażera to matematyczny koszmar. Przy zaledwie 20 miastach liczba możliwych tras wynosi 121 kwadrylionów. Klasyczne komputery "pocą się" przy próbie sprawdzenia każdej z nich. Algorytm Genetyczny nie sprawdza wszystkich dróg – on uczy się znajdować te najlepsze, kopiując mechanizmy znane z biologii: dziedziczenie, mutacje i walkę o przetrwanie.

1. Anatomia Cyfrowej Ewolucji

Algorytm genetyczny nie operuje na jednej trasie. On zarządza całym "społeczeństwem" rozwiązań, które z każdą sekundą staje się mądrzejsze.

1. Populacja

Zaczynamy od totalnego chaosu. Tworzymy setki losowych tras. Każda z nich to "osobnik", a kolejność miast w tej trasie to jego "DNA". Większość z nich na początku jest beznadziejna, ale to nasz punkt startowy.

2. Selekcja

To cyfrowy Darwinizm. Oceniamy każdą trasę i przyznajemy jej punkty (Fitness). Im krótsza droga, tym większa szansa, że ten osobnik zostanie "rodzicem" i przekaże swoje geny do następnego pokolenia.

3. Krzyżowanie

Łączymy dwie dobre trasy, tworząc "potomka". Wybieramy np. połowę drogi od taty i uzupełniamy ją miastami w kolejności, w jakiej występowały u mamy. W ten sposób łączymy najlepsze cechy obu rozwiązań.

4. Mutacja

To iskra innowacji. Czasami w DNA nowej trasy losowo zamieniamy dwa miasta miejscami. Dzięki temu algorytm nie wpada w rutynę i może odkryć genialne połączenie, którego nie mieli jego przodkowie.

⭐ Elitaryzm: Strażnik Postępu

W czystej ewolucji może zdarzyć się pech – mutacja może zniszczyć genialną trasę, którą udało się wypracować po tysiącach prób. Elitaryzm to mechanizm bezpieczeństwa.

  • Gwarancja jakości: Kopiujemy 1-2 absolutnie najlepszych osobników bezpośrednio do następnej ery, bez żadnych zmian.
  • Brak regresu: Dzięki temu rekordowo krótka trasa nigdy nie zostanie utracona, dopóki nie znajdziemy jeszcze lepszej.
  • Szybsza zbieżność: Cała populacja ma "wzór do naśladowania", wokół którego może ewoluować.

2. Wizualizacja: Ewolucja w akcji

Poniżej widzisz symulację GA. Cienkie linie to "duchy" odrzuconych możliwości, a świecąca, neonowa linia to aktualny Lider Populacji (dzięki elityzmowi). Zauważ, jak licznik generacji rośnie, a trasa staje się coraz bardziej logiczna.

3. Implementacja w Pythonie (z Elityzmem)

Poniżej znajdziesz profesjonalną strukturę algorytmu. Zwróć uwagę na funkcję evolve, gdzie elity są traktowane priorytetowo.

genetic_optimizer.py Python / Logic
import random
import numpy as np

class GeneticTSP:
    def __init__(self, cities, pop_size=100, elite_size=2):
        self.cities = cities
        self.pop_size = pop_size
        self.elite_size = elite_size
        # Populacja to teraz lista par: [trasa, fitness]
        self.population = []
        for _ in range(pop_size):
            tour = random.sample(range(len(cities)), len(cities))
            self.population.append([tour, self.calculate_fitness(tour)])

    def calculate_fitness(self, tour):
        """Oblicza fitness tylko raz dla danej trasy."""
        dist = 0
        for i in range(len(tour)):
            c1 = self.cities[tour[i]]
            c2 = self.cities[tour[(i + 1) % len(tour)]]
            # Formuła euklidesowa: sqrt((x1-x2)^2 + (y1-y2)^2)
            dist += np.sqrt((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2)
        return 1 / dist

    def evolve(self):
        # 1. Sortujemy gotowe wyniki (fitness mamy już policzony)
        self.population.sort(key=lambda x: x[1], reverse=True)
        
        # 2. Elityzm
        new_pop = self.population[:self.elite_size]
        
        # 3. Rozmnażanie
        while len(new_pop) < self.pop_size:
            # Wybieramy rodziców (pobieramy tylko trasy z par [trasa, fitness])
            p1_data, p2_data = random.choices(self.population[:50], k=2)
            parent1, parent2 = p1_data[0], p2_data[0]
            
            child_tour = self.crossover(parent1, parent2)
            
            if random.random() < 0.05: # Zwiększyłem szansę na mutację do 5%
                self.mutate(child_tour)
            
            # Obliczamy fitness dla NOWEGO dziecka raz i zapisujemy w parze
            new_pop.append([child_tour, self.calculate_fitness(child_tour)])
            
        self.population = new_pop

    def crossover(self, p1, p2):
        a, b = sorted(random.sample(range(len(p1)), 2))
        child = [None] * len(p1)
        child[a:b] = p1[a:b]
        
        ptr = 0
        for city in p2:
            if city not in child:
                while child[ptr] is not None: 
                    ptr += 1
                child[ptr] = city
        return child

    def mutate(self, tour):
        i, j = random.sample(range(len(tour)), 2)
        tour[i], tour[j] = tour[j], tour[i]

    def get_best(self):
        # Zwraca najlepszą trasę i jej dystans
        best_tour, best_fitness = self.population[0]
        return best_tour, 1 / best_fitness

4. Matematyka "Przetrwania Najsilniejszych"

Fundamentem jest Funkcja Przystosowania (Fitness Function). Musi ona jednoznacznie premiować krótsze rozwiązania, aby ewolucja wiedziała, w którym kierunku podążać.

f(P) = 1 / Σ dist(ci, ci+1)

Maksymalizacja f(P) powoduje, że algorytm naturalnie dąży do minimalizacji mianownika, czyli całkowitej długości trasy.

5. Zastosowania w przemyśle

Choć nazwa brzmi jak dylemat sprzedawcy, ten algorytm steruje dzisiejszym światem:

  • Logistyka: Planowanie tras kurierów DHL czy InPost (oszczędności rzędu milionów litrów paliwa).
  • Mikroprocesory: Optymalizacja ścieżek lasera przy wypalaniu obwodów scalonych.
  • Astronomia: Planowanie ruchu teleskopów kosmicznych między obserwowanymi obiektami.