| | """TSP Algorithm - Nearest Neighbor + 2-opt optimization. |
| | |
| | Optimizes route for a list of places to minimize total travel distance. |
| | Uses Haversine formula for distance calculation. |
| | """ |
| |
|
| | from math import radians, sin, cos, sqrt, atan2 |
| |
|
| |
|
| | def haversine(lat1: float, lng1: float, lat2: float, lng2: float) -> float: |
| | """ |
| | Calculate distance between 2 points on Earth in kilometers. |
| | |
| | Uses the Haversine formula for great-circle distance. |
| | |
| | Args: |
| | lat1, lng1: First point coordinates |
| | lat2, lng2: Second point coordinates |
| | |
| | Returns: |
| | Distance in kilometers |
| | """ |
| | R = 6371 |
| | |
| | dlat = radians(lat2 - lat1) |
| | dlng = radians(lng2 - lng1) |
| | |
| | a = sin(dlat/2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlng/2)**2 |
| | c = 2 * atan2(sqrt(a), sqrt(1-a)) |
| | |
| | return R * c |
| |
|
| |
|
| | def calculate_distance_matrix(places: list[dict]) -> list[list[float]]: |
| | """ |
| | Build NxN distance matrix for all place pairs. |
| | |
| | Args: |
| | places: List of places with 'lat' and 'lng' keys |
| | |
| | Returns: |
| | NxN matrix where matrix[i][j] is distance from place i to j |
| | """ |
| | n = len(places) |
| | matrix = [[0.0] * n for _ in range(n)] |
| | |
| | for i in range(n): |
| | for j in range(n): |
| | if i != j: |
| | matrix[i][j] = haversine( |
| | places[i]['lat'], places[i]['lng'], |
| | places[j]['lat'], places[j]['lng'] |
| | ) |
| | |
| | return matrix |
| |
|
| |
|
| | def nearest_neighbor(matrix: list[list[float]], start: int = 0) -> list[int]: |
| | """ |
| | Greedy nearest neighbor heuristic for TSP. |
| | |
| | Builds tour by always visiting the closest unvisited city. |
| | O(n²) complexity. |
| | |
| | Args: |
| | matrix: Distance matrix |
| | start: Starting city index |
| | |
| | Returns: |
| | List of city indices in visit order |
| | """ |
| | n = len(matrix) |
| | visited = [False] * n |
| | tour = [start] |
| | visited[start] = True |
| | |
| | for _ in range(n - 1): |
| | current = tour[-1] |
| | nearest = -1 |
| | min_dist = float('inf') |
| | |
| | for j in range(n): |
| | if not visited[j] and matrix[current][j] < min_dist: |
| | min_dist = matrix[current][j] |
| | nearest = j |
| | |
| | if nearest != -1: |
| | tour.append(nearest) |
| | visited[nearest] = True |
| | |
| | return tour |
| |
|
| |
|
| | def two_opt(tour: list[int], matrix: list[list[float]]) -> list[int]: |
| | """ |
| | 2-opt local search improvement for TSP. |
| | |
| | Iteratively reverses segments to reduce total distance. |
| | Continues until no improvement is found. |
| | |
| | Args: |
| | tour: Initial tour (list of indices) |
| | matrix: Distance matrix |
| | |
| | Returns: |
| | Improved tour |
| | """ |
| | n = len(tour) |
| | if n < 4: |
| | return tour |
| | |
| | improved = True |
| | tour = tour.copy() |
| | |
| | while improved: |
| | improved = False |
| | for i in range(1, n - 1): |
| | for j in range(i + 1, n): |
| | if j == n - 1: |
| | |
| | d1 = matrix[tour[i-1]][tour[i]] + matrix[tour[j]][tour[0]] |
| | d2 = matrix[tour[i-1]][tour[j]] + matrix[tour[i]][tour[0]] |
| | else: |
| | d1 = matrix[tour[i-1]][tour[i]] + matrix[tour[j]][tour[j+1]] |
| | d2 = matrix[tour[i-1]][tour[j]] + matrix[tour[i]][tour[j+1]] |
| | |
| | if d2 < d1 - 0.0001: |
| | |
| | tour[i:j+1] = tour[i:j+1][::-1] |
| | improved = True |
| | |
| | return tour |
| |
|
| |
|
| | def calculate_total_distance(tour: list[int], matrix: list[list[float]]) -> float: |
| | """Calculate total distance of a tour.""" |
| | total = 0.0 |
| | for i in range(len(tour) - 1): |
| | total += matrix[tour[i]][tour[i+1]] |
| | return total |
| |
|
| |
|
| | def optimize_route(places: list[dict], start_index: int = 0) -> tuple[list[int], float]: |
| | """ |
| | Main TSP optimization function. |
| | |
| | Uses Nearest Neighbor heuristic followed by 2-opt improvement. |
| | Suitable for up to ~50 places. |
| | |
| | Args: |
| | places: List of places with 'lat' and 'lng' keys |
| | start_index: Index of starting place (default: first place) |
| | |
| | Returns: |
| | Tuple of (optimized_order, total_distance_km) |
| | - optimized_order: List of indices in visit order |
| | - total_distance_km: Total route distance |
| | """ |
| | n = len(places) |
| | |
| | |
| | if n == 0: |
| | return [], 0.0 |
| | if n == 1: |
| | return [0], 0.0 |
| | if n == 2: |
| | dist = haversine( |
| | places[0]['lat'], places[0]['lng'], |
| | places[1]['lat'], places[1]['lng'] |
| | ) |
| | return [0, 1], dist |
| | |
| | |
| | matrix = calculate_distance_matrix(places) |
| | |
| | |
| | tour = nearest_neighbor(matrix, start_index) |
| | |
| | |
| | tour = two_opt(tour, matrix) |
| | |
| | |
| | total = calculate_total_distance(tour, matrix) |
| | |
| | return tour, round(total, 2) |
| |
|
| |
|
| | def estimate_duration(distance_km: float, avg_speed_kmh: float = 30) -> int: |
| | """ |
| | Estimate travel duration in minutes. |
| | |
| | Args: |
| | distance_km: Total distance in km |
| | avg_speed_kmh: Average speed (default: 30 km/h for city driving) |
| | |
| | Returns: |
| | Estimated duration in minutes |
| | """ |
| | hours = distance_km / avg_speed_kmh |
| | return int(hours * 60) |
| |
|