Emtix

Grafos Ponderados y Optimización

Publicado el 1 de diciembre de 2025

Algoritmos en Grafos Ponderados: La Base de la Optimización de Redes

Los grafos ponderados transforman problemas complejos de optimización en estructuras manejables. Desde diseñar redes de telecomunicaciones hasta planificar rutas de entrega, estos algoritmos son fundamentales en ingeniería moderna.


1. Definición: Grafos Ponderados

Grafo Ponderado

Un grafo ponderado es un grafo donde cada arista tiene un peso (valor numérico) asociado.

Notación: G=(V,E,w)G = (V, E, w) donde:

  • VV = vértices
  • EE = aristas
  • w:ERw: E \to \mathbb{R} = función de peso

Los pesos representan costos reales: distancia, tiempo, costo monetario, capacidad, etc.

Contraste con Grafos No Ponderados

CaracterísticaNo PonderadoPonderado
Peso de aristaImplícito (todas = 1)Explícito (cada arista tiene peso)
Camino más cortoMenor número de aristasMenor suma de pesos
Algoritmo búsquedaBFS funcionaRequiere Dijkstra/Bellman-Ford
EjemploRed socialRed de carreteras con distancias

Representación en Código

Implementación de grafo ponderado
class GrafoPonderado:
    """Grafo ponderado usando lista de adyacencia"""
    def __init__(self, dirigido=False):
        self.dirigido = dirigido
        self.adyacencia = {}
    
    def agregar_vertice(self, v):
        """Agregar un vértice"""
        if v not in self.adyacencia:
            self.adyacencia[v] = []
    
    def agregar_arista(self, u, v, peso):
        """Agregar arista ponderada de u a v"""
        self.agregar_vertice(u)
        self.agregar_vertice(v)
        
        # Agregar arista u -> v con peso
        self.adyacencia[u].append((v, peso))
        
        # Si no dirigido, agregar v -> u
        if not self.dirigido:
            self.adyacencia[v].append((u, peso))
    
    def obtener_vecinos(self, v):
        """Obtener vecinos con sus pesos"""
        return self.adyacencia.get(v, [])
    
    def __repr__(self):
        lineas = [f"Grafo Ponderado ({'Dirigido' if self.dirigido else 'No Dirigido'}):"]
        for vertice, vecinos in self.adyacencia.items():
            vecinos_str = ', '.join([f"{v}({p})" for v, p in vecinos])
            lineas.append(f"  {vertice} -> [{vecinos_str}]")
        return '\n'.join(lineas)

# Ejemplo: Red de ciudades con distancias

red_ciudades = GrafoPonderado(dirigido=False)

# Agregar conexiones con distancias en km

red_ciudades.agregar_arista('Madrid', 'Barcelona', 620)
red_ciudades.agregar_arista('Madrid', 'Valencia', 355)
red_ciudades.agregar_arista('Madrid', 'Sevilla', 530)
red_ciudades.agregar_arista('Barcelona', 'Valencia', 350)
red_ciudades.agregar_arista('Valencia', 'Sevilla', 650)

print(red_ciudades)

print("\n" + "="\*60)
print("Vecinos de Madrid:")
for ciudad, distancia in red_ciudades.obtener_vecinos('Madrid'):
print(f" → {ciudad}: {distancia} km")
Quiz

¿Qué representa el peso de una arista en un grafo ponderado?


2. Propósito: Problemas de Optimización

Objetivo Fundamental

Desarrollar algoritmos eficientes para resolver problemas de optimización en redes:

  • Minimizar costos de conexión total
  • Encontrar rutas óptimas entre puntos
  • Maximizar flujo a través de redes
  • Optimizar asignación de recursos

Dos Problemas Fundamentales

Árbol de Expansión Mínima (MST)

Minimum Spanning Tree

Problema: Conectar todos los vértices usando la menor suma de pesos posible.

Aplicaciones:

  • Diseño de redes de telecomunicaciones
  • Sistemas de distribución de agua/electricidad
  • Diseño de circuitos impresos
  • Clustering de datos

Camino Más Corto (Shortest Path)

Shortest Path

Problema: Encontrar la ruta que minimiza la suma de pesos entre dos puntos.

Aplicaciones:

  • Navegación GPS
  • Enrutamiento de paquetes en Internet
  • Logística y entrega
  • Planificación de rutas aéreas

Ejemplo Visual

Comparación MST vs Shortest Path
Grafo original:
              5
         A -------- B
         |   \   /  |
         |    \ /   |
       1 |     X    | 6
         |    / \   |
         |   /   \  |
         C -------- D
              3

MST (conexión mínima):
  - Aristas seleccionadas: A-C(1), C-D(3), C-B(2)
  - Costo total: 1 + 3 + 2 = 6
  - Conecta todos los vértices con costo mínimo

Camino más corto (A → D):
  - Ruta: A → C → D
  - Costo: 1 + 3 = 4
  - Es el camino de menor costo de A a D

3. Árbol de Expansión Mínima (MST)

Minimum Spanning Tree

Un MST es un subconjunto de aristas que:

  1. Forma un árbol (conectado y sin ciclos)
  2. Incluye todos los vértices
  3. Tiene la suma mínima de pesos

Para un grafo con nn vértices, el MST tiene exactamente n1n-1 aristas.

Propiedades del MST

  • Unicidad: Si todos los pesos son distintos, el MST es único
  • Optimalidad: Cualquier árbol de expansión con menor costo sería el MST
  • Greedy funciona: Los algoritmos codiciosos encuentran el óptimo

4. Algoritmo de Prim

Algoritmo de Prim

Estrategia greedy: Crecer el árbol una arista a la vez, siempre eligiendo la arista de menor peso que conecta el árbol con un vértice nuevo.

Complejidad: O(n2)O(n^2) con implementación simple, O((m+n)logn)O((m+n) \log n) con heap.

Procedimiento

  1. Iniciar con un vértice arbitrario en el árbol
  2. Repetir hasta incluir todos los vértices:
  • Encontrar la arista de menor peso que conecta el árbol con un vértice fuera
  • Agregar esa arista y vértice al árbol
  1. Resultado: MST completo
Algoritmo de Prim - Implementación completa
import heapq

def prim_mst(grafo, inicio):
    """
    Algoritmo de Prim para MST

    Args:
        grafo: GrafoPonderado
        inicio: vértice inicial

    Returns:
        mst_aristas: lista de aristas en el MST
        costo_total: suma de pesos del MST
    """
    visitados = set()
    mst_aristas = []
    costo_total = 0

    # Min-heap: (peso, vertice_origen, vertice_destino)
    heap = []

    # Iniciar con el vértice inicial
    visitados.add(inicio)
    for vecino, peso in grafo.obtener_vecinos(inicio):
        heapq.heappush(heap, (peso, inicio, vecino))

    print(f"Iniciando Prim desde {inicio}\n")

    while heap and len(visitados) < len(grafo.adyacencia):
        peso, u, v = heapq.heappop(heap)

        # Si v ya está en el árbol, saltar
        if v in visitados:
            continue

        # Agregar arista al MST
        visitados.add(v)
        mst_aristas.append((u, v, peso))
        costo_total += peso

        print(f"Agregando arista: {u} -- {v} (peso: {peso})")
        print(f"  Costo acumulado: {costo_total}")

        # Agregar aristas desde v a vértices no visitados
        for vecino, peso_vecino in grafo.obtener_vecinos(v):
            if vecino not in visitados:
                heapq.heappush(heap, (peso_vecino, v, vecino))

    return mst_aristas, costo_total

# Ejemplo: Red de distribución eléctrica
print("="*60)
print("ALGORITMO DE PRIM: Red de Distribución Eléctrica")
print("="*60 + "\n")

red_electrica = GrafoPonderado(dirigido=False)

# Casas a conectar con costos de cableado
conexiones = [
    ('Casa A', 'Casa B', 4),
    ('Casa A', 'Casa C', 2),
    ('Casa B', 'Casa C', 1),
    ('Casa B', 'Casa D', 5),
    ('Casa C', 'Casa D', 8),
    ('Casa C', 'Casa E', 10),
    ('Casa D', 'Casa E', 2),
    ('Casa D', 'Casa F', 6),
    ('Casa E', 'Casa F', 3),
]

for u, v, costo in conexiones:
    red_electrica.agregar_arista(u, v, costo)

print("Red original:")
print(red_electrica)

print("\n" + "="*60)
print("Ejecutando Algoritmo de Prim:")
print("="*60 + "\n")

mst_aristas, costo_total = prim_mst(red_electrica, 'Casa A')

print("\n" + "="*60)
print("RESULTADO:")
print("="*60)
print(f"\nÁrbol de Expansión Mínima (MST):")
for u, v, peso in mst_aristas:
    print(f"  {u} -- {v}: {peso} unidades")

print(f"\nCosto total mínimo: {costo_total} unidades")
print(f"Aristas en MST: {len(mst_aristas)}")
print(f"Ahorro respecto a todas las conexiones: "
      f"{sum(c for _, _, c in conexiones) - costo_total} unidades")

Grafo inicial:

         A --- 4 --- B
         |           |
         2           5
         |           |
         C --- 1 --- D
         |           |
        10           2
         |           |
         E --- 3 --- F
                     |
                     6

Ejecución paso a paso desde A:

Paso 1: Iniciar
  En árbol: {A}
  Candidatos: A-B(4), A-C(2)
  Elegir: A-C (peso mínimo: 2)

Paso 2:
  En árbol: {A, C}
  Candidatos: A-B(4), C-B(1), C-D(8), C-E(10)
  Elegir: C-B (peso mínimo: 1)

Paso 3:
  En árbol: {A, C, B}
  Candidatos: A-B(4-desc), B-D(5), C-D(8), C-E(10)
  Elegir: B-D (peso mínimo: 5)

Paso 4:
  En árbol: {A, C, B, D}
  Candidatos: C-D(8-desc), C-E(10), D-E(2), D-F(6)
  Elegir: D-E (peso mínimo: 2)

Paso 5:
  En árbol: {A, C, B, D, E}
  Candidatos: C-E(10-desc), E-F(3), D-F(6)
  Elegir: E-F (peso mínimo: 3)

Finalizado!

MST resultante:

         A
         |
         2
         |
         C --- 1 --- B
                     |
                     5
                     |
                     D
                     |
                     2
                     |
                     E --- 3 --- F

Costo total: 2 + 1 + 5 + 2 + 3 = 13


5. Algoritmo de Kruskal

Algoritmo de Kruskal

Estrategia greedy: Ordenar todas las aristas por peso y agregarlas una por una, saltando las que crearían ciclos.

Complejidad: O(mlogm)O(m \log m) donde mm = número de aristas.

Diferencia Clave con Prim

CaracterísticaPrimKruskal
InicioRequiere vértice inicialNo requiere inicio
EstrategiaCrecer árbol conectadoUnir componentes
Estructura auxiliarPriority queueUnion-Find
Mejor paraGrafos densosGrafos esparsos

Union-Find: Estructura de Datos Clave

Union-Find (Disjoint Set)

Estructura de datos que mantiene una colección de conjuntos disjuntos y soporta:

  • find(x): Encontrar el representante del conjunto de x
  • union(x, y): Unir los conjuntos de x e y

Con compresión de caminos y unión por rango: O(α(n))O(\alpha(n))O(1)O(1) amortizado.

Algoritmo de Kruskal con Union-Find
class UnionFind:
    """
    Estructura Union-Find (Disjoint Set)
    con compresión de caminos y unión por rango
    """
    def __init__(self, elementos):
        self.padre = {e: e for e in elementos}
        self.rango = {e: 0 for e in elementos}
    
    def find(self, x):
        """Encontrar el representante con compresión de caminos"""
        if self.padre[x] != x:
            self.padre[x] = self.find(self.padre[x])  # Compresión
        return self.padre[x]
    
    def union(self, x, y):
        """Unir dos conjuntos por rango"""
        raiz_x = self.find(x)
        raiz_y = self.find(y)
        
        if raiz_x == raiz_y:
            return False  # Ya están en el mismo conjunto
        
        # Unión por rango
        if self.rango[raiz_x] < self.rango[raiz_y]:
            self.padre[raiz_x] = raiz_y
        elif self.rango[raiz_x] > self.rango[raiz_y]:
            self.padre[raiz_y] = raiz_x
        else:
            self.padre[raiz_y] = raiz_x
            self.rango[raiz_x] += 1
        
        return True

def kruskal_mst(grafo):
"""
Algoritmo de Kruskal para MST

    Args:
        grafo: GrafoPonderado

    Returns:
        mst_aristas: lista de aristas en el MST
        costo_total: suma de pesos del MST
    """
    # Obtener todas las aristas
    aristas = []
    for u in grafo.adyacencia:
        for v, peso in grafo.obtener_vecinos(u):
            # Evitar duplicados en grafos no dirigidos
            if u < v:
                aristas.append((peso, u, v))

    # Ordenar aristas por peso
    aristas.sort()

    # Inicializar Union-Find
    vertices = list(grafo.adyacencia.keys())
    uf = UnionFind(vertices)

    mst_aristas = []
    costo_total = 0

    print("Procesando aristas en orden de peso:\n")

    for peso, u, v in aristas:
        # Verificar si u y v están en diferentes componentes
        if uf.union(u, v):
            mst_aristas.append((u, v, peso))
            costo_total += peso
            print(f"✓ Agregar: {u} -- {v} (peso: {peso})")
            print(f"  Costo acumulado: {costo_total}")
        else:
            print(f"✗ Saltar: {u} -- {v} (peso: {peso}) - crearía ciclo")

        # Terminar si ya tenemos n-1 aristas
        if len(mst_aristas) == len(vertices) - 1:
            break

    return mst_aristas, costo_total

# Ejemplo: Red de fibra óptica

print("="*60)
print("ALGORITMO DE KRUSKAL: Red de Fibra Óptica")
print("="*60 + "\n")

red_fibra = GrafoPonderado(dirigido=False)

# Oficinas a conectar con costos de instalación

conexiones_fibra = [
('Oficina A', 'Oficina B', 7),
('Oficina A', 'Oficina D', 5),
('Oficina B', 'Oficina C', 8),
('Oficina B', 'Oficina D', 9),
('Oficina B', 'Oficina E', 7),
('Oficina C', 'Oficina E', 5),
('Oficina D', 'Oficina E', 15),
('Oficina D', 'Oficina F', 6),
('Oficina E', 'Oficina F', 8),
('Oficina E', 'Oficina G', 9),
('Oficina F', 'Oficina G', 11),
]

for u, v, costo in conexiones_fibra:
red_fibra.agregar_arista(u, v, costo)

print("Red original:")
for u, v, costo in conexiones_fibra:
print(f" {u} -- {v}: {costo}")

print("\n" + "="*60)
print("Ejecutando Algoritmo de Kruskal:")
print("="*60 + "\n")

mst_aristas_k, costo_total_k = kruskal_mst(red_fibra)

print("\n" + "="*60)
print("RESULTADO:")
print("="*60)
print(f"\nÁrbol de Expansión Mínima (MST):")
for u, v, peso in mst_aristas_k:
print(f" {u} -- {v}: {peso} unidades")

print(f"\nCosto total mínimo: {costo_total_k} unidades")
print(f"Aristas en MST: {len(mst_aristas_k)}")
Quiz

¿Cuál es la diferencia principal entre Prim y Kruskal?


6. Camino Más Corto: Algoritmo de Dijkstra

Algoritmo de Dijkstra

Encuentra el camino más corto desde un vértice fuente a todos los demás vértices en grafos con pesos no negativos.

Complejidad: O((m+n)logn)O((m+n) \log n) con heap binario.

Estrategia

Similar a Prim, pero en lugar de minimizar el peso de la arista, minimiza la distancia total desde la fuente.

  1. Iniciar con distancia 0 en la fuente, infinito en otros
  2. Repetir:
  • Seleccionar el vértice no visitado con menor distancia
  • Actualizar distancias de sus vecinos (relajación)
  1. Resultado: Árbol de caminos más cortos desde la fuente
Algoritmo de Dijkstra - Implementación completa
import heapq

def dijkstra(grafo, inicio):
    """
    Algoritmo de Dijkstra para caminos más cortos

    Args:
        grafo: GrafoPonderado
        inicio: vértice inicial

    Returns:
        distancias: dict con distancias mínimas desde inicio
        padres: dict para reconstruir caminos
    """
    # Inicializar distancias y padres
    distancias = {v: float('inf') for v in grafo.adyacencia}
    distancias[inicio] = 0
    padres = {inicio: None}
    visitados = set()

    # Min-heap: (distancia, vértice)
    heap = [(0, inicio)]

    print(f"Iniciando Dijkstra desde {inicio}\n")

    while heap:
        dist_actual, u = heapq.heappop(heap)

        # Si ya visitado, saltar
        if u in visitados:
            continue

        visitados.add(u)
        print(f"Procesando {u} (distancia: {dist_actual})")

        # Relajar aristas
        for v, peso in grafo.obtener_vecinos(u):
            if v in visitados:
                continue

            nueva_dist = distancias[u] + peso

            # Si encontramos un camino mejor, actualizar
            if nueva_dist < distancias[v]:
                distancias[v] = nueva_dist
                padres[v] = u
                heapq.heappush(heap, (nueva_dist, v))
                print(f"  Relajar {u} -> {v}: distancia = {nueva_dist}")

    return distancias, padres

def reconstruir_camino(padres, inicio, fin):
    """Reconstruir camino desde inicio a fin"""
    if fin not in padres:
        return None

    camino = []
    actual = fin
    while actual is not None:
        camino.append(actual)
        actual = padres.get(actual)

    camino.reverse()
    return camino

# Ejemplo: Red de transporte
print("="*60)
print("ALGORITMO DE DIJKSTRA: Red de Transporte")
print("="*60 + "\n")

red_transporte = GrafoPonderado(dirigido=True)

# Ciudades con tiempos de viaje (minutos)
rutas = [
    ('A', 'B', 10),
    ('A', 'C', 3),
    ('B', 'C', 1),
    ('B', 'D', 2),
    ('C', 'B', 4),
    ('C', 'D', 8),
    ('C', 'E', 2),
    ('D', 'E', 7),
    ('E', 'D', 9),
]

for origen, destino, tiempo in rutas:
    red_transporte.agregar_arista(origen, destino, tiempo)

print("Red de transporte:")
for origen, destino, tiempo in rutas:
    print(f"  {origen}{destino}: {tiempo} min")

print("\n" + "="*60)
print("Ejecutando Dijkstra desde A:")
print("="*60 + "\n")

distancias, padres = dijkstra(red_transporte, 'A')

print("\n" + "="*60)
print("RESULTADOS:")
print("="*60 + "\n")

print("Distancias mínimas desde A:")
for destino in sorted(distancias.keys()):
    dist = distancias[destino]
    if dist == float('inf'):
        print(f"  A → {destino}: INALCANZABLE")
    else:
        camino = reconstruir_camino(padres, 'A', destino)
        camino_str = ' → '.join(camino)
        print(f"  A → {destino}: {dist} min")
        print(f"    Ruta: {camino_str}")

Advertencia Crítica: Pesos Negativos

¡ADVERTENCIA! Dijkstra NO funciona con pesos negativos

El algoritmo de Dijkstra solo garantiza corrección si todos los pesos son no negativos (w(e)0)(w(e) \geq 0).

Problemas con pesos negativos:

  • Una arista negativa puede hacer que un camino ya procesado sea subóptimo
  • Un ciclo de costo negativo impide la convergencia (se puede reducir infinitamente)

Alternativa: Usar Bellman-Ford (funciona con pesos negativos, detecta ciclos negativos).

Grafo con peso negativo:

         A --- 5 ---> B
         |           /
         |         /
       2 |      -4
         |     /
         v   v
         C

Ejecución incorrecta de Dijkstra:

1. Iniciar: dist[A]=0, dist[B]=inf, dist[C]=inf
2. Procesar A:
  - Relajar A→B: dist[B]=5
  - Relajar A→C: dist[C]=2

3. Procesar C (dist=2, menor):
  - Relajar C→B: dist[B] = 2 + (-4) = -2 (¡mejor!)

4. Procesar B:
  - Pero B ya fue procesado indirectamente
  - Dijkstra no revisita B

Resultado: dist[B] = 5 (INCORRECTO, debería ser -2)

¿Por qué falla?

Dijkstra asume que una vez que un vértice es procesado, su distancia es definitiva. Con pesos negativos, un camino más largo en número de aristas puede tener menor costo total.

Solución: Usar Bellman-Ford

Bellman-Ford relaja todas las aristas n1n-1 veces, garantizando que encuentra el camino más corto incluso con pesos negativos.


7. Caminos Entre Todos los Pares: Floyd-Warshall

Algoritmo de Floyd-Warshall

Calcula los caminos más cortos entre TODOS los pares de vértices.

Complejidad: O(n3)O(n^3)

Ventajas:

  • ✅ Funciona con pesos negativos (sin ciclos negativos)
  • ✅ Detecta ciclos de costo negativo
  • ✅ Simple de implementar

Estrategia: Programación Dinámica

El algoritmo considera progresivamente vértices intermedios:

  • dist[i][j][k] = distancia más corta de ii a jj usando solo vértices {1,2,...,k}\{1, 2, ..., k\} como intermedios

Recurrencia:

dist[i][j][k] = min(dist[i][j][k-1], dist[i][k][k-1] + dist[k][j][k-1])

Es decir: o no usamos kk, o usamos kk como intermedio.

Algoritmo de Floyd-Warshall - Implementación
def floyd_warshall(grafo):
    """
    Algoritmo de Floyd-Warshall para todos los pares
    
    Args:
        grafo: GrafoPonderado
    
    Returns:
        dist: matriz de distancias
        next: matriz para reconstruir caminos
    """
    vertices = list(grafo.adyacencia.keys())
    n = len(vertices)
    
    # Mapeo de vértices a índices
    v_to_idx = {v: i for i, v in enumerate(vertices)}
    idx_to_v = {i: v for v, i in v_to_idx.items()}
    
    # Inicializar matrices
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]
    next_vertex = [[None] * n for _ in range(n)]
    
    # Distancia de un vértice a sí mismo es 0
    for i in range(n):
        dist[i][i] = 0
    
    # Inicializar con aristas directas
    for u in grafo.adyacencia:
        i = v_to_idx[u]
        for v, peso in grafo.obtener_vecinos(u):
            j = v_to_idx[v]
            dist[i][j] = peso
            next_vertex[i][j] = j
    
    print(f"Ejecutando Floyd-Warshall ({n} vértices)...\n")
    
    # Algoritmo principal: considerar cada vértice como intermedio
    for k in range(n):
        print(f"Iteración {k+1}/{n}: Usando {idx_to_v[k]} como intermedio")
        for i in range(n):
            for j in range(n):
                # ¿Es mejor pasar por k?
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
                    next_vertex[i][j] = next_vertex[i][k]
    
    # Detectar ciclos negativos
    tiene_ciclo_negativo = False
    for i in range(n):
        if dist[i][i] < 0:
            tiene_ciclo_negativo = True
            break
    
    return dist, next_vertex, v_to_idx, idx_to_v, tiene_ciclo_negativo

def reconstruir_camino_floyd(next_vertex, i, j, idx_to_v):
"""Reconstruir camino de i a j usando matriz next"""
if next_vertex[i][j] is None:
return None

    camino = [idx_to_v[i]]
    while i != j:
        i = next_vertex[i][j]
        camino.append(idx_to_v[i])

    return camino

# Ejemplo: Red de vuelos

print("="*60)
print("ALGORITMO DE FLOYD-WARSHALL: Red de Vuelos")
print("="*60 + "\n")

red_vuelos = GrafoPonderado(dirigido=True)

# Vuelos con precios

vuelos = [
('Madrid', 'Barcelona', 50),
('Madrid', 'Valencia', 40),
('Barcelona', 'París', 80),
('Valencia', 'Barcelona', 30),
('Valencia', 'París', 120),
('París', 'Londres', 60),
('Barcelona', 'Londres', 100),
]

for origen, destino, precio in vuelos:
red_vuelos.agregar_arista(origen, destino, precio)

print("Vuelos disponibles:")
for origen, destino, precio in vuelos:
print(f" {origen}{destino}: ${precio}")

print("\n" + "="*60)
dist, next_v, v_to_idx, idx_to_v, ciclo_neg = floyd_warshall(red_vuelos)
print("="*60 + "\n")

if ciclo_neg:
print("⚠️ ERROR: Ciclo de costo negativo detectado")
else:
print("MATRIZ DE DISTANCIAS (Todos los pares):\n")

    # Encabezado
    vertices = list(v_to_idx.keys())
    print(f"{'Desde/A':<12}", end="")
    for v in vertices:
        print(f"{v:<12}", end="")
    print()
    print("-" * (12 * (len(vertices) + 1)))

    # Matriz
    for v1 in vertices:
        i = v_to_idx[v1]
        print(f"{v1:<12}", end="")
        for v2 in vertices:
            j = v_to_idx[v2]
            d = dist[i][j]
            if d == float('inf'):
                print(f"{'∞':<12}", end="")
            else:
                print(f"${d:<11}", end="")
        print()

    print("\n" + "="*60)
    print("EJEMPLOS DE RUTAS ÓPTIMAS:")
    print("="*60 + "\n")

    ejemplos = [('Madrid', 'Londres'), ('Valencia', 'Londres'), ('Madrid', 'París')]

    for origen, destino in ejemplos:
        i = v_to_idx[origen]
        j = v_to_idx[destino]
        d = dist[i][j]

        if d == float('inf'):
            print(f"{origen}{destino}: INALCANZABLE")
        else:
            camino = reconstruir_camino_floyd(next_v, i, j, idx_to_v)
            camino_str = ' → '.join(camino)
            print(f"{origen}{destino}: ${d}")
            print(f"  Ruta: {camino_str}\n")
Quiz

¿Cuándo es preferible usar Floyd-Warshall sobre ejecutar Dijkstra n veces?


8. Aplicación Práctica: Modelado de Problemas

Principio Fundamental

“Diseñar grafos, no algoritmos”

El arte está en modelar el problema correctamente como un grafo. Una vez modelado, la solución es estándar.

Ejemplo 1: Optimización de Testing Robótico (MST)

Problema Real

Contexto: Testing de placas de circuito impreso (PCB)

Objetivo: Optimizar el movimiento de un brazo robótico que verifica la conectividad de puntos de contacto.

Modelado:

  1. Vértices: Puntos de contacto en el PCB
  2. Aristas: Todas las conexiones posibles (grafo completo)
  3. Pesos: Tiempo de movimiento del robot entre dos puntos
Cálculo de peso con métrica L-infinito
def distancia_robot(p1, p2):
    """
    Distancia L-infinito (Chebyshev)
    El robot se mueve en X e Y simultáneamente
    Tiempo = max(|dx|, |dy|)
    """
    dx = abs(p1[0] - p2[0])
    dy = abs(p1[1] - p2[1])
    return max(dx, dy)

# Ejemplo: Puntos de contacto en PCB
puntos_contacto = {
    'P1': (0, 0),
    'P2': (3, 4),
    'P3': (6, 1),
    'P4': (2, 5),
    'P5': (8, 3),
}

# Crear grafo completo ponderado
grafo_pcb = GrafoPonderado(dirigido=False)

for p1, coord1 in puntos_contacto.items():
    for p2, coord2 in puntos_contacto.items():
        if p1 < p2:
            peso = distancia_robot(coord1, coord2)
            grafo_pcb.agregar_arista(p1, p2, peso)

print("="*60)
print("OPTIMIZACIÓN DE TESTING ROBÓTICO")
print("="*60 + "\n")

print("Puntos de contacto:")
for punto, (x, y) in puntos_contacto.items():
    print(f"  {punto}: ({x}, {y})")

print("\nDistancias de movimiento (L∞):")
for p1, coord1 in puntos_contacto.items():
    for p2, coord2 in puntos_contacto.items():
        if p1 < p2:
            d = distancia_robot(coord1, coord2)
            print(f"  {p1}{p2}: {d} unidades")

# Aplicar Prim para MST
print("\n" + "="*60)
print("Aplicando Prim para optimizar ruta:")
print("="*60 + "\n")

mst_robot, tiempo_total = prim_mst(grafo_pcb, 'P1')

print("\n" + "="*60)
print("SOLUCIÓN:")
print("="*60)
print(f"\nSecuencia óptima de movimientos:")
for u, v, tiempo in mst_robot:
    print(f"  {u}{v}: {tiempo} unidades")

print(f"\nTiempo total minimizado: {tiempo_total} unidades")

# Para clustering: romper MST en k grupos
print("\n" + "="*60)
print("CLUSTERING (romper MST en 2 grupos):")
print("="*60)

# Encontrar la arista más pesada en el MST
arista_mas_pesada = max(mst_robot, key=lambda x: x[2])
print(f"\nRemover arista más pesada: {arista_mas_pesada[0]} -- {arista_mas_pesada[1]} ({arista_mas_pesada[2]})")

# Los dos clusters resultantes
cluster1 = {arista_mas_pesada[0]}
cluster2 = {arista_mas_pesada[1]}

for u, v, _ in mst_robot:
    if (u, v) == (arista_mas_pesada[0], arista_mas_pesada[1]):
        continue
    if u in cluster1 or v in cluster1:
        cluster1.add(u)
        cluster1.add(v)
    else:
        cluster2.add(u)
        cluster2.add(v)

print(f"\nCluster 1: {cluster1}")
print(f"Cluster 2: {cluster2}")

Ejemplo 2: Desambiguación de Texto T9 (Shortest Path)

Problema Real

Contexto: Teclado T9 (teléfonos antiguos)

Objetivo: Reconstruir texto donde cada tecla representa múltiples letras.

Modelado:

  1. Vértices: Posibles palabras para cada posición
  2. Aristas: Transiciones entre palabras consecutivas
  3. Pesos: Probabilidad negativa (menor = más probable)
Desambiguación T9 con shortest path
import math

# Mapeo T9

T9_MAP = {
'2': 'ABC', '3': 'DEF', '4': 'GHI',
'5': 'JKL', '6': 'MNO', '7': 'PQRS',
'8': 'TUV', '9': 'WXYZ'
}

# Probabilidades de bigramas (simplificado)

BIGRAM_PROB = {
('THE', 'CAT'): 0.01,
('THE', 'DOG'): 0.02,
('A', 'CAT'): 0.005,
('A', 'DOG'): 0.008,
}

def decodificar_t9(codigo):
"""Obtener posibles letras para un código T9"""
return T9_MAP.get(codigo, '')

def palabras_posibles(secuencia_t9, diccionario):
"""Obtener palabras del diccionario que coinciden con secuencia T9"""
posibles = []
for palabra in diccionario:
if len(palabra) != len(secuencia_t9):
continue

        coincide = True
        for i, codigo in enumerate(secuencia_t9):
            if palabra[i] not in decodificar_t9(codigo):
                coincide = False
                break

        if coincide:
            posibles.append(palabra)

    return posibles

def peso_transicion(palabra1, palabra2):
"""
Peso de transición entre dos palabras
Menor peso = más probable
""" # Obtener probabilidad del bigrama
prob = BIGRAM_PROB.get((palabra1, palabra2), 0.0001)

    # Convertir a peso (negativo del log)
    # Menor probabilidad = mayor peso
    return -math.log(prob)

# Ejemplo

print("="*60)
print("DESAMBIGUACIÓN DE TEXTO T9")
print("="*60 + "\n")

# Diccionario simplificado

diccionario = ['THE', 'A', 'CAT', 'DOG', 'SAT', 'RAT']

# Secuencia T9: "843 228" = "THE CAT" o "THE BAT" o...

secuencias = [
('843', ['THE']), # Solo una opción
('228', ['CAT']) # Podría ser CAT, BAT, AAT, etc.
]

print("Secuencia T9: 843 228")
print("\nDecodificación:")

grafo_t9 = GrafoPonderado(dirigido=True)

# Agregar vértice inicial

grafo_t9.agregar_vertice('START')

# Construir grafo de posibilidades

nivel_anterior = ['START']
for i, (seq, candidatos) in enumerate(secuencias):
print(f"\nPosición {i+1} ({seq}):")
posibles = palabras_posibles(seq, diccionario)
print(f" Posibles: {posibles}")

    for palabra_ant in nivel_anterior:
        for palabra_nueva in posibles:
            if palabra_ant == 'START':
                peso = 0
            else:
                peso = peso_transicion(palabra_ant, palabra_nueva)

            grafo_t9.agregar_arista(palabra_ant, palabra_nueva, peso)

    nivel_anterior = posibles

# Agregar vértice final

grafo_t9.agregar_vertice('END')
for palabra in nivel_anterior:
grafo_t9.agregar_arista(palabra, 'END', 0)

print("\n" + "="*60)
print("Aplicando Dijkstra para encontrar mejor interpretación:")
print("="*60 + "\n")

distancias, padres = dijkstra(grafo_t9, 'START')

print("\n" + "="*60)
print("RESULTADO:")
print("="*60)

# Reconstruir mejor camino

camino = reconstruir_camino(padres, 'START', 'END')
mejor_interpretacion = [p for p in camino if p not in ['START', 'END']]

print(f"\nMejor interpretación: {' '.join(mejor_interpretacion)}")
print(f"Costo total: {distancias['END']:.4f}")

1. Diseño de Redes de Distribución:

  • Problema: Conectar almacenes con tiendas
  • Modelo: MST con costos de transporte como pesos
  • Solución: Prim o Kruskal

2. Enrutamiento de Paquetes en Internet:

  • Problema: Encontrar ruta más rápida entre routers
  • Modelo: Shortest path con latencias como pesos
  • Solución: Dijkstra (protocolos OSPF, IS-IS)

3. Planificación de Rutas Aéreas:

  • Problema: Minimizar costo de viaje entre ciudades
  • Modelo: Shortest path con precios como pesos
  • Solución: Dijkstra o Floyd-Warshall

4. Diseño de Circuitos VLSI:

  • Problema: Conectar componentes minimizando longitud de cable
  • Modelo: Steiner Tree (generalización de MST)
  • Solución: Aproximación con MST

5. Análisis de Redes Sociales:

  • Problema: Encontrar influencers o información crítica
  • Modelo: Shortest paths con pesos de influencia
  • Solución: Floyd-Warshall o centralidad

6. Logística y Distribución:

  • Problema: Optimizar rutas de entrega
  • Modelo: TSP (Traveling Salesman Problem)
  • Solución: Aproximaciones con MST

9. Resumen Técnico y Comparación

Tabla Comparativa de Algoritmos

AlgoritmoProblemaComplejidadPesos NegativosMejor para
PrimMSTO(n2)O(n^2) o O((m+n)logn)O((m+n) \log n)N/AGrafos densos
KruskalMSTO(mlogm)O(m \log m)N/AGrafos esparsos
DijkstraShortest Path (single-source)O((m+n)logn)O((m+n) \log n)❌ NOPesos no negativos
Bellman-FordShortest Path (single-source)O(nm)O(nm)✅ SÍCon pesos negativos
Floyd-WarshallShortest Path (all-pairs)O(n3)O(n^3)✅ SÍGrafos pequeños/medianos

Guía de Selección

Éxito

¿Qué algoritmo usar?

Para MST:

  • Grafo denso → Prim
  • Grafo esparso → Kruskal

Para Shortest Path (un origen):

  • Pesos no negativos → Dijkstra
  • Pesos negativos → Bellman-Ford

Para Shortest Path (todos los pares):

  • Grafo pequeño/mediano → Floyd-Warshall
  • Grafo grande, sin pesos negativos → n × Dijkstra

Advertencias Críticas

Pesos Negativos

Dijkstra NO funciona con pesos negativos.

  • Una arista negativa puede invalidar caminos ya procesados
  • Un ciclo de costo negativo impide convergencia

Alternativas:

  • Bellman-Ford (detecta ciclos negativos)
  • Floyd-Warshall (para todos los pares)

10. Cierre: Principios de Diseño

Lecciones Clave

1. Modelar es Más Importante que Implementar

El éxito depende de la correcta configuración del modelo de grafo:

  • Identificar qué son los vértices
  • Identificar qué son las aristas
  • Definir pesos que reflejen fielmente el costo real

2. Una Vez Modelado, la Solución es Estándar

  • Minimizar costos de conexión → MST (Prim/Kruskal)
  • Rutas más rápidas/baratas → Shortest Path (Dijkstra/Floyd)
  • Flujo máximo → Ford-Fulkerson

3. Seleccionar la Estructura de Datos Correcta

  • Lista de adyacencia para la mayoría de casos
  • Matriz para Floyd-Warshall
  • Heap para Dijkstra/Prim eficientes

Estrategia de Resolución

Metodología para problemas de grafos ponderados
"""
METODOLOGÍA PARA RESOLVER PROBLEMAS CON GRAFOS PONDERADOS

1. IDENTIFICAR EL PROBLEMA
  - ¿Necesito conectar todos los vértices con costo mínimo? → MST
  - ¿Necesito la ruta más corta entre dos puntos? → Shortest Path
  - ¿Necesito rutas entre todos los pares? → All-pairs Shortest Path

2. MODELAR COMO GRAFO
  - Vértices: ¿Qué entidades del problema?
  - Aristas: ¿Qué conexiones existen?
  - Pesos: ¿Qué costo representa? (distancia, tiempo, $)

3. VERIFICAR PROPIEDADES
  - ¿Dirigido o no dirigido?
  - ¿Hay pesos negativos?
  - ¿Es denso o esparso?

4. SELECCIONAR ALGORITMO
  - MST denso → Prim
  - MST esparso → Kruskal
  - SP sin negativos → Dijkstra
  - SP con negativos → Bellman-Ford
  - All-pairs → Floyd-Warshall

5. IMPLEMENTAR Y VALIDAR
  - Usar estructuras de datos adecuadas
  - Verificar casos borde
  - Validar resultado
"""
Quiz

¿Cuál es el principio fundamental al trabajar con grafos ponderados?


Resumen Final

✓ Grafos Ponderados:

  • Definición: aristas con pesos numéricos
  • Representación con lista de adyacencia
  • Pesos representan costos reales

✓ MST (Árbol de Expansión Mínima):

  • Conecta todos los vértices con costo mínimo
  • n1n-1 aristas para nn vértices
  • Algoritmo de Prim: crecer árbol conectado
  • Algoritmo de Kruskal: unir componentes con Union-Find
  • Complejidad: O(mlogm)O(m \log m) (Kruskal) o O((m+n)logn)O((m+n) \log n) (Prim)

✓ Shortest Path (Camino Más Corto):

  • Dijkstra: un origen, pesos no negativos
  • Bellman-Ford: un origen, permite pesos negativos
  • Floyd-Warshall: todos los pares, O(n3)O(n^3)
  • Relajación de aristas

✓ Advertencias:

  • Dijkstra SOLO con pesos no negativos
  • Ciclos negativos impiden convergencia
  • Seleccionar algoritmo según propiedades del grafo

✓ Aplicaciones:

  • Redes de telecomunicaciones (MST)
  • Navegación y GPS (Dijkstra)
  • Logística y distribución
  • Diseño de circuitos
  • Análisis de redes

✓ Principio:

  • “Diseñar grafos, no algoritmos”
  • Modelado correcto es clave

¡Felicitaciones!

Has completado algoritmos en grafos ponderados. Ahora dominas:

  • La diferencia entre grafos ponderados y no ponderados
  • MST: Algoritmos de Prim y Kruskal
  • Shortest Path: Dijkstra y Floyd-Warshall
  • Cuándo usar cada algoritmo
  • Cómo modelar problemas reales como grafos
  • La importancia del modelado antes de la implementación
  • Advertencias críticas sobre pesos negativos

Próximo paso: Explorar algoritmos avanzados como flujo máximo, emparejamiento en grafos bipartitos, y problemas NP-completos en grafos.