El Desafío del Empaque en Contenedores: Optimizando Espacios en contenedores con Ingenio

Optimización del Embalaje en Contenedores: Algoritmos y Ejemplos en Python

Bin Packing explicado paso a paso: algoritmos, complejidad, código Python y cuándo usar cada estrategia.

Python Algoritmos Optimización NP-duro Logística

El problema de embalaje en contenedores (bin packing) es uno de los desafíos clásicos de la optimización combinatoria. Aparece en logística, planificación de servidores en la nube, corte de materiales y hasta en la asignación de anuncios publicitarios.

En este artículo veremos cuatro algoritmos para resolverlo, sus garantías teóricas, código Python listo para usar y una comparativa real para que elijas el correcto según tu escenario.

¿Qué es el Bin Packing y por qué es difícil?

Dado un conjunto de ítems con tamaños y contenedores de capacidad fija, el objetivo es usar el mínimo número de contenedores posible.

  • Entrada: lista de tamaños (ej. [4, 8, 1, 2, 5, 3]) y capacidad C (ej. 10).
  • Salida: asignación de ítems a contenedores que minimice el total.
⚠️ Es NP-duro: no existe un algoritmo eficiente (polinomial) que garantice la solución óptima para todos los casos. Por eso usamos heurísticas y metaheurísticas: encuentran buenas soluciones rápido, aunque no siempre la mejor posible.

Variantes: 1D (peso/volumen lineal), 2D (ancho × alto) y 3D. Este artículo se enfoca en 1D, la base de todas las demás.

First Fit Decreasing (FFD)

Idea: ordena los ítems de mayor a menor y coloca cada uno en el primer contenedor donde quepa. Si no cabe en ninguno, abre uno nuevo.

📐 Garantía matemática: FFD nunca usa más de (11/9)·OPT + 1 contenedores, donde OPT es el óptimo teórico. En la práctica suele estar muy cerca del óptimo real.
Python
def first_fit_decreasing(items, bin_capacity):
    # Paso 1: ordenar de mayor a menor
    sorted_items = sorted(items, reverse=True)
    bins = []  # lista de contenedores, cada uno es una lista de ítems

    for item in sorted_items:
        placed = False
        # Paso 2: buscar el primer contenedor con espacio
        for b in bins:
            if item <= bin_capacity - sum(b):
                b.append(item)
                placed = True
                break
        # Paso 3: si no cabe en ninguno, crear contenedor nuevo
        if not placed:
            bins.append([item])

    return len(bins), bins

# Ejemplo
items = [4, 8, 1, 2, 5, 3]
bin_capacity = 10
n, bins = first_fit_decreasing(items, bin_capacity)
print(f"Contenedores usados: {n}")  # → 3
print(f"Distribución: {bins}")      # → [[8], [5, 4], [3, 2, 1]]
💡 Cuándo usarlo: Es el estándar de oro para bin packing 1D. Rápido, fácil de implementar y con garantía matemática sólida. Úsalo siempre como baseline.

Best Fit

Idea: en lugar del primer contenedor que quepa, elige el que deje la menor capacidad restante después de colocar el ítem. Minimiza el espacio desperdiciado.

Python
def best_fit(items, bin_capacity):
    bins = []  # cada elemento es (capacidad_restante, [ítems])

    for item in items:
        best_idx = -1
        min_remaining = bin_capacity + 1

        # Buscar el contenedor con menor espacio sobrante
        for i, (rem, _) in enumerate(bins):
            if item <= rem and rem - item < min_remaining:
                best_idx = i
                min_remaining = rem - item

        if best_idx != -1:
            rem, content = bins[best_idx]
            bins[best_idx] = (rem - item, content + [item])
        else:
            bins.append((bin_capacity - item, [item]))

    result = [content for _, content in bins]
    return len(result), result

# Ejemplo
n, bins = best_fit(items, bin_capacity)
print(f"Contenedores usados: {n}")  # → 3
print(f"Distribución: {bins}")      # → [[8, 2], [5, 4], [3, 1]]
💡 Cuándo usarlo: Cuando el espacio desperdiciado cuesta dinero (contenedores marítimos, almacenes). Es más lento que FFD pero puede ahorrar contenedores en instancias con muchos ítems pequeños.

Algoritmos Genéticos

Idea: en lugar de seguir una regla fija, exploramos muchas soluciones posibles y las hacemos "evolucionar" mediante selección, cruce y mutación. Son metaheurísticas: no garantizan el óptimo, pero encuentran soluciones de alta calidad en problemas grandes donde las heurísticas clásicas fallan.

🧬 Representación: cada "cromosoma" es un orden de los ítems (permutación). Para evaluarlo, aplicamos Next Fit sobre ese orden. El fitness es el número de contenedores usados: menor es mejor.
Python
import random

def evaluate(order, items, bin_capacity):
    # Aplica Next Fit sobre el orden dado y cuenta contenedores
    bins = 1
    used = 0
    for idx in order:
        size = items[idx]
        if used + size <= bin_capacity:
            used += size
        else:
            bins += 1
            used = size
    return bins

def crossover(p1, p2):
    # Order Crossover (OX): preserva el orden relativo
    n = len(p1)
    a, b = sorted(random.sample(range(n), 2))
    child = [-1] * n
    child[a:b] = p1[a:b]
    remaining = [x for x in p2 if x not in child[a:b]]
    idx = 0
    for i in range(n):
        if child[i] == -1:
            child[i] = remaining[idx]
            idx += 1
    return child

def mutate(individual, rate=0.1):
    ind = individual[:]
    if random.random() < rate:
        i, j = random.sample(range(len(ind)), 2)
        ind[i], ind[j] = ind[j], ind[i]
    return ind

def genetic_algorithm(items, bin_capacity, pop_size=100,
                         generations=200, mut_rate=0.15):
    n = len(items)
    population = [random.sample(range(n), n) for _ in range(pop_size)]

    for gen in range(generations):
        # Evaluar fitness (menor número de bins = mejor)
        scores = [(evaluate(ind, items, bin_capacity), ind)
                  for ind in population]
        scores.sort(key=lambda x: x[0])

        # Seleccionar élite (top 20%)
        elite_count = pop_size // 5
        new_pop = [ind for _, ind in scores[:elite_count]]

        # Generar descendencia por cruce + mutación
        while len(new_pop) < pop_size:
            p1, p2 = random.sample(new_pop[:elite_count], 2)
            child = crossover(p1, p2)
            child = mutate(child, mut_rate)
            new_pop.append(child)

        population = new_pop

    best = min(population, key=lambda ind: evaluate(ind, items, bin_capacity))
    return evaluate(best, items, bin_capacity), best

# Ejemplo
num_bins, best_order = genetic_algorithm(items, bin_capacity)
print(f"Contenedores (GA): {num_bins}")  # → típicamente 3
💡 Cuándo usarlo: Instancias con más de 100 ítems donde FFD/Best Fit no son suficientes, o variantes 2D/3D donde no existen heurísticas simples. Requiere ajustar población y generaciones.

Comparativa de Algoritmos

O(n log n)

First Fit Decreasing

El estándar. Buen equilibrio entre calidad y velocidad. Garantía matemática comprobada.

O(n²)

Best Fit

Minimiza espacio desperdiciado. Útil cuando cada contenedor es caro.

O(n)

Next Fit

Ultra rápido. Ideal para streaming o memoria limitada. Peor calidad.

Variable

Genéticos

Para problemas grandes o 2D/3D. Requiere tuning. No garantiza óptimo.

AlgoritmoTiempoCalidadMemoriaCuándo usar
FFDRápido⭐⭐⭐⭐BajaCaso general, baseline obligatorio
Best FitMedio⭐⭐⭐⭐MediaContenedores caros, muchos ítems
Next FitMuy rápido⭐⭐MínimaStreaming, tiempo real, memoria limitada
GenéticosLento⭐⭐⭐⭐⭐AltaInstancias grandes, 2D/3D, óptimo cercano

Resultados con el mismo ejemplo

Para los ítems [4, 8, 1, 2, 5, 3] con capacidad 10:

AlgoritmoBinsDistribuciónEspacio desperdiciado
FFD3[8], [5, 4], [3, 2, 1]2 + 1 + 4 = 7
Best Fit3[8, 2], [5, 4], [3, 1]0 + 1 + 6 = 7
Next Fit4[4], [8], [1, 2, 5], [3]6 + 2 + 2 + 7 = 17
Genético3Variable (≈ óptimo)≈ 6-7
🔍 Insight: FFD y Best Fit empatan en número de contenedores, pero Best Fit deja menos espacio suelto dentro de cada uno. Next Fit pierde por no reutilizar bins cerrados. El genético encuentra el mismo resultado que FFD en este ejemplo pequeño, pero brilla cuando hay decenas o cientos de ítems.

Conclusión y Recomendaciones

El bin packing es un problema fundamental que aparece en más lugares de los que parece: desde camiones de reparto hasta asignación de máquinas virtuales en AWS.

  • Empieza siempre con FFD. Es rápido, simple y tiene garantía matemática. Si la solución es suficiente, detente ahí.
  • Usa Best Fit si cada contenedor tiene un costo significativo y necesitas exprimir cada byte.
  • Next Fit solo en sistemas embebidos o streaming donde la memoria es crítica.
  • Genéticos cuando el problema crece (100+ ítems) o cuando trabajas en 2D/3D.

¿Tienes un caso real para aplicar estos algoritmos? ¡Déjalo en los comentarios! 👇

¿Te gustó este artículo?

Suscríbete · Comparte · LinkedIn

Entradas populares de este blog

Análisis ABC en Power BI con DAX.

Cómo Realizar One-Hot Encoding en Power BI.

Cómo Equilibrar Múltiples Productos con Pedidos a la Medida

Descubriendo el Poder de los Modelos de Clasificación en Machine Learning: Predicciones Precisas y Clasificaciones Sorprendentes

Maximiza la rentabilidad de tu negocio: Cómo optimizar la selección de proveedores de mercancías.

Target Encoding en Power BI: La Guía Definitiva Sin Data Leakage

Optimización del Inventario Multiproducto en Espacios Reducidos: Una guía para la eficiencia en gestión de stocks

¡Plan Desagregado de Producción como un jefe!

Domina tu Almacén sin arruinarte: El Juego del Modelo de Inventario Múltiproductos con Presupuesto ajustado