Held-Karp¶
El algoritmo de Held-Karp, también llamado algoritmo de Bellman-Held-Karp, es un método de programación dinámica propuesto para resolver el Problema del Viajante de Comercio (TSP). La entrada es una matriz de distancias entre ciudades, y el objetivo es encontrar el recorrido de costo mínimo que visite cada ciudad exactamente una vez y regrese al punto de partida.
Idea principal del algoritmo¶
El enfoque se basa en dividir el problema en subproblemas más pequeños:
- En vez de calcular directamente el camino mínimo que pasa por todas las ciudades, se define una función de costo parcial llamada:
\(g(S, e)\)
donde:
- \(S\) es un subconjunto de ciudades que deben visitarse (sin incluir la ciudad inicial).
- \(e\) es la ciudad final del recorrido parcial.
- \(g(S,e)\) representa el camino más corto que comienza en la ciudad inicial \(1\), pasa por todas las ciudades de \(S\) y termina en \(e\).
Etapas del algoritmo¶
1. Inicialización (casos base)¶
- Si \(S = \emptyset\), entonces:
\(g(\emptyset, e) = d(1, e)\)
es decir, el costo es simplemente la distancia directa de la ciudad inicial \(1\) a la ciudad \(e\).
Ejemplo:
- \(g(\emptyset, 2) = d(1,2)\)
- \(g(\emptyset, 3) = d(1,3)\)
2. Construcción de soluciones parciales¶
Para un subconjunto \(S = {s_1, s_2, \ldots, s_k}\) y una ciudad destino \(e \notin S\), el algoritmo observa cuál debería ser la penúltima ciudad antes de llegar a \(e\). La recurrencia es:
\(g(S, e) = \min_{s_i \in S} ; \big[ g(S \setminus {s_i}, s_i) + d(s_i, e) \big]\)
Esto significa:
- El costo óptimo para llegar a \(e\) pasando por \(S\) es el mínimo entre todos los caminos que llegan primero a algún \(s_i \in S\) y luego avanzan a \(e\).
Ejemplo:
- Para \(S={2,3}, e=4\), tenemos:
\(g({2,3}, 4) = \min { g({3},2)+d(2,4),; g({2},3)+d(3,4) }\)
3. Finalización¶
Cuando todos los subconjuntos han sido evaluados, obtenemos el costo mínimo del ciclo Hamiltoniano resolviendo:
\(\text{OPT} = \min_{k \in {2,\ldots,n}} \big[ g({2,\ldots,n} \setminus {k}, k) + d(k,1) \big]\)
Es decir:
- Consideramos todos los caminos que parten de la ciudad inicial, pasan por todas las ciudades, terminan en alguna ciudad (k), y finalmente vuelven a la ciudad inicial.
- El mínimo entre ellos es la solución óptima.
Reconstrucción del camino¶
Para obtener no solo el costo sino también el recorrido completo, el algoritmo almacena en cada \(g(S, e)\) cuál fue la penúltima ciudad óptima.
- Esto permite retroceder desde el resultado final y reconstruir paso a paso el camino óptimo.
- El costo en memoria aumenta solo en un factor constante, ya que se guarda un índice adicional.
Complejidad¶
- Tiempo: \(O(n^2 \cdot 2^n)\).
- Espacio: \(O(n \cdot 2^n)\).
Esto lo hace viable solo para \((n \leq 20)\) aproximadamente. Para conjuntos más grandes de ciudades se usan algoritmos heurísticos o aproximados.
Código en Python¶
from scipy.spatial.distance import pdist, squareform
from itertools import combinations
def held_karp(cities, num_cities):
dist = squareform(pdist(cities))
return held_karp_dist(dist)
def held_karp_dist(dist: List[List[float]]) -> Tuple[float, List[int]]:
"""Solves the TSP using Held-Karp Dynamic Programming algorithm.
This is an exact algorithm with O(n^2 * 2^n) time and space complexity.
Suitable only for small n (n <= 20).
Args:
dist: 2D matrix (n x n) of costs between cities.
Returns:
Tuple: (minimum tour cost, tour as list of city indices)
"""
path = cities = list(range(len(dist)))
min_cost = 1.0
n = len(dist)
# g[S][k] = costo mínimo de ir de 0 a k pasando por todos en S
# Usamos un diccionario: clave (frozenset(S), k)
g = {}
# Caso base: conjuntos de tamaño 1
for k in range(1, n):
g[(frozenset([k]), k)] = dist[0][k]
# Construcción por tamaño de subconjunto
for s in range(2, n):
for S in combinations(range(1, n), s):
S = frozenset(S)
for k in S:
prev_S = S - {k}
g[(S, k)] = min(
g[(prev_S, m)] + dist[m][k] for m in prev_S
)
# Cierre del ciclo
all_nodes = frozenset(range(1, n))
opt_cost, last_node = min(
(g[(all_nodes, k)] + dist[k][0], k) for k in range(1, n)
)
# Reconstrucción del camino
tour = [0]
S = all_nodes
k = last_node
while S:
tour.append(k)
prev_S = S - {k}
# buscamos de dónde vino k
k = min(prev_S, key=lambda m: g[(prev_S, m)] + dist[m][tour[-1]]) if prev_S else 0
S = prev_S
tour.append(0)
return opt_cost, tour
Explicación del código Held-Karp en Python¶
from scipy.spatial.distance import pdist, squareform
from itertools import combinations
-
Importaciones:
-
pdist: calcula todas las distancias por pares entre puntos (ciudades). squareform: convierte esas distancias en una matriz cuadrada (n x n).combinations: genera todas las combinaciones posibles de un conjunto de ciudades.
def held_karp(cities, num_cities):
dist = squareform(pdist(cities))
return held_karp_dist(dist)
- Se define una función de interfaz
held_karp. -
Entrada:
-
cities: coordenadas de las ciudades en un plano (ejemplo: lista de pares (x,y)). num_cities: cantidad de ciudades (no se usa directamente, porque se infiere decities).- Convierte las coordenadas en una matriz de distancias con
pdistysquareform. - Luego llama a la función auxiliar
held_karp_dist, que resuelve el problema usando programación dinámica.
def held_karp_dist(dist: List[List[float]]) -> Tuple[float, List[int]]:
"""Solves the TSP using Held-Karp Dynamic Programming algorithm.
This is an exact algorithm with O(n^2 * 2^n) time and space complexity.
Suitable only for small n (n <= 20).
"""
- Esta es la función principal donde se implementa Held-Karp.
-
Entrada:
-
dist: matriz de distancias (n x n). -
Salida:
-
(costo mínimo, recorrido óptimo).
path = cities = list(range(len(dist)))
min_cost = 1.0
n = len(dist)
-
Se inicializan variables:
-
cities = [0, 1, ..., n-1]: lista con los índices de las ciudades. pathse iguala acities(por ahora, solo como referencia inicial).min_cost = 1.0: valor de partida (placeholder, será sobrescrito).n: número de ciudades.
# g[S][k] = costo mínimo de ir de 0 a k pasando por todos en S
# Usamos un diccionario: clave (frozenset(S), k)
g = {}
- Se define la estructura de DP (
g). g[(S, k)]almacena el costo mínimo para empezar en la ciudad0, visitar todas las ciudades en el conjuntoS, y terminar en la ciudadk.- Se usan
frozenset(S)porque son inmutables y pueden usarse como clave de diccionario.
# Caso base: conjuntos de tamaño 1
for k in range(1, n):
g[(frozenset([k]), k)] = dist[0][k]
-
Inicialización de la DP:
-
Si el conjunto (S = {k}) tiene un solo nodo, el costo mínimo de ir de 0 a k es simplemente
dist[0][k]. - Se llena
gcon los costos base.
# Construcción por tamaño de subconjunto
for s in range(2, n):
for S in combinations(range(1, n), s):
S = frozenset(S)
for k in S:
prev_S = S - {k}
g[(S, k)] = min(
g[(prev_S, m)] + dist[m][k] for m in prev_S
)
-
Construcción recursiva de la tabla DP:
-
Se generan subconjuntos
Sde tamaños. - Para cada ciudad destino
kenS, se considera quekes la última ciudad visitada. - El costo se calcula como:
\(g(S,k) = \min_{m \in S \setminus {k}} \Big( g(S \setminus {k}, m) + d(m,k) \Big)\)
- Es decir, buscamos la penúltima ciudad
mque minimiza el recorrido hastak.
# Cierre del ciclo
all_nodes = frozenset(range(1, n))
opt_cost, last_node = min(
(g[(all_nodes, k)] + dist[k][0], k) for k in range(1, n)
)
- Una vez calculados todos los subproblemas, cerramos el ciclo volviendo a la ciudad inicial (0).
- Se prueba cada ciudad
kcomo la última antes de regresar a0. - El costo total es:
\(\text{costo}(k) = g({1,\dots,n-1}, k) + d(k,0)\)
opt_cost: el costo mínimo encontrado.last_node: ciudad final óptima antes de volver a 0.
# Reconstrucción del camino
tour = [0]
S = all_nodes
k = last_node
while S:
tour.append(k)
prev_S = S - {k}
# buscamos de dónde vino k
k = min(prev_S, key=lambda m: g[(prev_S, m)] + dist[m][tour[-1]]) if prev_S else 0
S = prev_S
tour.append(0)
- Aquí se reconstruye el camino óptimo (no solo el costo).
-
Se comienza desde
last_nodey se va retrocediendo: -
En cada paso se busca qué ciudad
mfue la penúltima que llevó al costo mínimo. - Se añade
kal recorrido. - Se actualiza el conjunto
Squitando la ciudad ya usada. - Finalmente, se agrega la ciudad inicial
0al final para cerrar el ciclo.
return opt_cost, tour
-
Devuelve una tupla con:
-
opt_cost: el costo del tour mínimo. tour: la secuencia de ciudades visitadas.
Resumen del funcionamiento del código¶
- Convierte las coordenadas en una matriz de distancias.
- Inicializa los casos base con un solo nodo.
- Usa programación dinámica para calcular el costo mínimo de visitar conjuntos cada vez más grandes.
- Encuentra el mejor ciclo Hamiltoniano cerrando el camino en la ciudad inicial.
- Reconstruye la ruta óptima a partir de las decisiones almacenadas implícitamente en la DP.