Introducción

La optimización combinatoria es una rama de la optimización matemática que se centra en problemas donde el objetivo es encontrar el mejor objeto de un conjunto finito de objetos. Estos problemas son comunes en diversas áreas como la logística, la planificación, las redes y la bioinformática. En este tema, exploraremos varias técnicas y algoritmos utilizados para resolver problemas de optimización combinatoria.

Conceptos Clave

  1. Problema de Optimización Combinatoria: Un problema donde se busca encontrar el mejor objeto (solución) de un conjunto finito de objetos.
  2. Espacio de Soluciones: El conjunto de todas las posibles soluciones de un problema.
  3. Función Objetivo: Una función que asigna un valor a cada solución en el espacio de soluciones, indicando su calidad.
  4. Restricciones: Condiciones que deben cumplir las soluciones válidas.

Algoritmos Clásicos

  1. Algoritmo de Fuerza Bruta

El algoritmo de fuerza bruta evalúa todas las posibles soluciones y selecciona la mejor. Aunque es simple y garantiza encontrar la solución óptima, es ineficiente para problemas grandes debido a su complejidad exponencial.

Ejemplo: Problema del Viajante (TSP)

from itertools import permutations

def calculate_distance(path, distance_matrix):
    return sum(distance_matrix[path[i]][path[i + 1]] for i in range(len(path) - 1))

def tsp_brute_force(distance_matrix):
    n = len(distance_matrix)
    all_permutations = permutations(range(n))
    min_distance = float('inf')
    best_path = None
    for perm in all_permutations:
        current_distance = calculate_distance(perm + (perm[0],), distance_matrix)
        if current_distance < min_distance:
            min_distance = current_distance
            best_path = perm
    return best_path, min_distance

# Ejemplo de uso
distance_matrix = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]
best_path, min_distance = tsp_brute_force(distance_matrix)
print(f"Mejor ruta: {best_path}, Distancia mínima: {min_distance}")

  1. Algoritmo de Ramificación y Poda (Branch and Bound)

Este algoritmo explora el espacio de soluciones de manera más eficiente que la fuerza bruta, utilizando una estructura de árbol y podando ramas que no pueden contener la solución óptima.

Ejemplo: Problema de la Mochila (Knapsack)

def knapsack_branch_and_bound(weights, values, capacity):
    n = len(weights)
    max_value = 0
    best_combination = []

    def bound(i, current_weight, current_value):
        if current_weight >= capacity:
            return 0
        upper_bound = current_value
        total_weight = current_weight
        for j in range(i, n):
            if total_weight + weights[j] <= capacity:
                total_weight += weights[j]
                upper_bound += values[j]
            else:
                upper_bound += (capacity - total_weight) * values[j] / weights[j]
                break
        return upper_bound

    def branch_and_bound(i, current_weight, current_value, current_combination):
        nonlocal max_value, best_combination
        if i == n:
            if current_value > max_value:
                max_value = current_value
                best_combination = current_combination[:]
            return
        if current_weight + weights[i] <= capacity:
            current_combination.append(i)
            branch_and_bound(i + 1, current_weight + weights[i], current_value + values[i], current_combination)
            current_combination.pop()
        if bound(i + 1, current_weight, current_value) > max_value:
            branch_and_bound(i + 1, current_weight, current_value, current_combination)

    branch_and_bound(0, 0, 0, [])
    return best_combination, max_value

# Ejemplo de uso
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5
best_combination, max_value = knapsack_branch_and_bound(weights, values, capacity)
print(f"Mejor combinación: {best_combination}, Valor máximo: {max_value}")

  1. Algoritmo de Recocido Simulado (Simulated Annealing)

Este algoritmo es una técnica probabilística para aproximar la solución óptima de un problema. Se basa en el proceso de recocido en metalurgia, donde un material se calienta y luego se enfría lentamente para eliminar defectos.

Ejemplo: Problema del Viajante (TSP)

import random
import math

def tsp_simulated_annealing(distance_matrix, initial_temp, cooling_rate):
    def calculate_distance(path):
        return sum(distance_matrix[path[i]][path[i + 1]] for i in range(len(path) - 1))

    def get_neighbour(path):
        a, b = random.sample(range(len(path)), 2)
        path[a], path[b] = path[b], path[a]
        return path

    n = len(distance_matrix)
    current_path = list(range(n))
    random.shuffle(current_path)
    current_distance = calculate_distance(current_path + [current_path[0]])
    best_path = current_path[:]
    best_distance = current_distance
    temp = initial_temp

    while temp > 1:
        new_path = get_neighbour(current_path[:])
        new_distance = calculate_distance(new_path + [new_path[0]])
        if new_distance < current_distance or math.exp((current_distance - new_distance) / temp) > random.random():
            current_path = new_path
            current_distance = new_distance
            if new_distance < best_distance:
                best_path = new_path
                best_distance = new_distance
        temp *= cooling_rate

    return best_path, best_distance

# Ejemplo de uso
distance_matrix = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]
initial_temp = 1000
cooling_rate = 0.99
best_path, best_distance = tsp_simulated_annealing(distance_matrix, initial_temp, cooling_rate)
print(f"Mejor ruta: {best_path}, Distancia mínima: {best_distance}")

Ejercicios Prácticos

Ejercicio 1: Problema del Viajante (TSP) con Fuerza Bruta

Descripción: Implementa el algoritmo de fuerza bruta para resolver el problema del viajante con una matriz de distancias diferente.

Matriz de distancias:

[
    [0, 29, 20, 21],
    [29, 0, 15, 17],
    [20, 15, 0, 28],
    [21, 17, 28, 0]
]

Solución:

from itertools import permutations

def calculate_distance(path, distance_matrix):
    return sum(distance_matrix[path[i]][path[i + 1]] for i in range(len(path) - 1))

def tsp_brute_force(distance_matrix):
    n = len(distance_matrix)
    all_permutations = permutations(range(n))
    min_distance = float('inf')
    best_path = None
    for perm in all_permutations:
        current_distance = calculate_distance(perm + (perm[0],), distance_matrix)
        if current_distance < min_distance:
            min_distance = current_distance
            best_path = perm
    return best_path, min_distance

# Ejemplo de uso
distance_matrix = [
    [0, 29, 20, 21],
    [29, 0, 15, 17],
    [20, 15, 0, 28],
    [21, 17, 28, 0]
]
best_path, min_distance = tsp_brute_force(distance_matrix)
print(f"Mejor ruta: {best_path}, Distancia mínima: {min_distance}")

Ejercicio 2: Problema de la Mochila con Ramificación y Poda

Descripción: Implementa el algoritmo de ramificación y poda para resolver el problema de la mochila con diferentes pesos y valores.

Pesos: [1, 2, 3, 8] Valores: [20, 5, 10, 40] Capacidad: 10

Solución:

def knapsack_branch_and_bound(weights, values, capacity):
    n = len(weights)
    max_value = 0
    best_combination = []

    def bound(i, current_weight, current_value):
        if current_weight >= capacity:
            return 0
        upper_bound = current_value
        total_weight = current_weight
        for j in range(i, n):
            if total_weight + weights[j] <= capacity:
                total_weight += weights[j]
                upper_bound += values[j]
            else:
                upper_bound += (capacity - total_weight) * values[j] / weights[j]
                break
        return upper_bound

    def branch_and_bound(i, current_weight, current_value, current_combination):
        nonlocal max_value, best_combination
        if i == n:
            if current_value > max_value:
                max_value = current_value
                best_combination = current_combination[:]
            return
        if current_weight + weights[i] <= capacity:
            current_combination.append(i)
            branch_and_bound(i + 1, current_weight + weights[i], current_value + values[i], current_combination)
            current_combination.pop()
        if bound(i + 1, current_weight, current_value) > max_value:
            branch_and_bound(i + 1, current_weight, current_value, current_combination)

    branch_and_bound(0, 0, 0, [])
    return best_combination, max_value

# Ejemplo de uso
weights = [1, 2, 3, 8]
values = [20, 5, 10, 40]
capacity = 10
best_combination, max_value = knapsack_branch_and_bound(weights, values, capacity)
print(f"Mejor combinación: {best_combination}, Valor máximo: {max_value}")

Conclusión

En esta sección, hemos explorado varios algoritmos de optimización combinatoria, incluyendo fuerza bruta, ramificación y poda, y recocido simulado. Cada uno de estos algoritmos tiene sus propias ventajas y desventajas, y la elección del algoritmo adecuado depende de la naturaleza del problema y las restricciones de tiempo y recursos. En los ejercicios prácticos, hemos aplicado estos algoritmos a problemas clásicos como el problema del viajante y el problema de la mochila, proporcionando una comprensión más profunda de cómo funcionan en la práctica.

En el siguiente módulo, profundizaremos en los algoritmos de optimización combinatoria más avanzados, como los algoritmos genéticos y la optimización de colonia de hormigas.

© Copyright 2024. Todos los derechos reservados