cv.cm

Programación · Algoritmos

Algoritmos voraces explicados: cómo funcionan, código y ejemplos

6 min de lectura

Respuesta corta

Un algoritmo voraz (greedy) construye una solución paso a paso y en cada paso toma la opción que parece mejor en ese momento, sin volver atrás para cambiar decisiones anteriores. Es rápido y simple, pero solo es correcto en problemas con la propiedad de elección voraz, en los que una elección localmente óptima forma siempre parte de alguna solución óptima. Entre los algoritmos voraces correctos clásicos están la planificación de intervalos, la codificación de Huffman, el algoritmo de Dijkstra y los algoritmos de Kruskal y Prim para el árbol de expansión mínima.

Qué problema resuelven los algoritmos voraces

Los algoritmos voraces atacan problemas de optimización: elegir el mayor número de actividades, usar el menor número de monedas, construir la red más barata, comprimir un archivo en el menor número de bits. En lugar de explorar todas las combinaciones, un algoritmo voraz ordena o prioriza las opciones y se compromete con la mejor en cada paso. Eso suele dar un tiempo O(n log n), a menudo solo el coste de ordenar.

Lo difícil no es el código sino la demostración. Muchas reglas voraces naturales son incorrectas, así que debes demostrar que la regla es correcta o contrastarla con una solución de fuerza bruta.

La idea en palabras sencillas

  1. Encuentra una regla que ordene las opciones, por ejemplo "primero la que termina antes" o "primero la arista más ligera".
  2. Ordena las opciones según esa regla o guárdalas en una cola de prioridad.
  3. Recorre las opciones y toma cada una que las restricciones todavía permitan.
  4. Nunca deshagas una elección.

Un algoritmo voraz es correcto cuando se cumplen dos propiedades. Propiedad de elección voraz: alguna solución óptima contiene la primera elección voraz. Subestructura óptima: tras tomar esa elección, el resto del problema es una instancia más pequeña del mismo problema. La demostración habitual es un argumento de intercambio: toma cualquier solución óptima y muestra que cambiar una de sus elecciones por la elección voraz no la empeora.

Ejemplo resuelto: planificación de intervalos

Tienes una sala de reuniones y estas solicitudes (inicio, fin): A (1, 4), B (3, 5), C (0, 6), D (5, 7), E (3, 9), F (5, 9), G (6, 10), H (8, 11). Reserva el mayor número posible de reuniones sin solapamientos. La regla voraz correcta es: elegir siempre la reunión que termina antes entre las que empiezan después de que termine la última reservada.

Reunión (ordenada por fin)Inicio, fin¿Empieza después del último fin?DecisiónÚltimo fin
A1, 4sí (nada reservado)Reservar4
B3, 5no (3 < 4)Omitir4
C0, 6noOmitir4
D5, 7síReservar7
E3, 9noOmitir7
F5, 9noOmitir7
G6, 10noOmitir7
H8, 11síReservar11

Resultado: A, D, H, tres reuniones, que es el máximo. Por qué funciona: la reunión que termina antes deja más tiempo libre para todo lo demás, así que sustituir la primera reunión de cualquier horario óptimo por ella nunca provoca un conflicto.

Implementación en Python

def max_meetings(meetings):
    """meetings: list of (name, start, end). Returns the booked names."""
    booked, last_end = [], float("-inf")
    for name, start, end in sorted(meetings, key=lambda m: m[2]):
        if start >= last_end:
            booked.append(name)
            last_end = end
    return booked

requests = [("A", 1, 4), ("B", 3, 5), ("C", 0, 6), ("D", 5, 7),
            ("E", 3, 9), ("F", 5, 9), ("G", 6, 10), ("H", 8, 11)]
print(max_meetings(requests))  # ['A', 'D', 'H']

Un segundo ejemplo clásico es la mochila fraccionaria: cuando puedes tomar parte de un objeto, tomar los objetos en orden de valor por unidad de peso es óptimo.

def fractional_knapsack(items, capacity):
    """items: list of (value, weight). Returns the best total value."""
    total = 0.0
    for value, weight in sorted(items, key=lambda i: i[0] / i[1], reverse=True):
        take = min(weight, capacity)
        total += value * take / weight
        capacity -= take
        if capacity == 0:
            break
    return total

print(fractional_knapsack([(60, 10), (100, 20), (120, 30)], 50))  # 240.0

Cuando el voraz falla: contraejemplos

ProblemaRegla voraz tentadoraContraejemploMétodo correcto
Cambio de monedas, monedas 1, 3, 4Tomar primero la moneda más grandeCantidad 6: el voraz da 4 + 1 + 1 (3 monedas), lo óptimo es 3 + 3 (2 monedas)Programación dinámica
Mochila 0/1 (solo objetos enteros)Primero el mejor valor por pesoCapacidad 50, objetos (60, 10), (100, 20), (120, 30): el voraz toma 60 + 100 = 160, lo óptimo es 100 + 120 = 220Programación dinámica
Planificación de intervalosPrimero la reunión más corta(0, 5), (4, 7), (6, 11): el voraz toma solo (4, 7), lo óptimo toma las otras dosPrimero la que termina antes
Camino más corto con aristas negativasPrimero el nodo más cercano de DijkstraCualquier grafo en el que una arista negativa abarata un nodo ya finalizadoBellman-Ford

Complejidad temporal y espacial

AlgoritmoTiempoMotivo
Planificación de intervalosO(n log n)Ordenación por hora de fin; el recorrido es O(n).
Mochila fraccionariaO(n log n)Ordenación por valor por peso.
Codificación de HuffmanO(n log n)n − 1 fusiones, cada una con operaciones de montículo.
MST de KruskalO(E log E)Ordenar las aristas más un union-find de coste casi constante.
Caminos más cortos de DijkstraO((V + E) log V)Cola de prioridad basada en montículo.

El espacio suele ser O(n) para la copia ordenada o el montículo. Si la entrada ya está ordenada, muchos algoritmos voraces se ejecutan en O(n).

Cuándo usar un algoritmo voraz y usos en el mundo real

  • Usa un algoritmo voraz cuando puedas demostrar la propiedad de elección voraz, o cuando una respuesta aproximada rápida sea aceptable. En caso contrario, usa programación dinámica o backtracking.
  • Compresión: la codificación de Huffman construye códigos de prefijo óptimos fusionando repetidamente los dos símbolos menos frecuentes; se usa dentro de formatos como DEFLATE (ZIP, gzip, PNG).
  • Redes: árboles de expansión mínima (Kruskal, Prim) y enrutamiento por caminos más cortos (Dijkstra).
  • Planificación: primero el plazo más próximo (earliest deadline first) y reserva de salas o máquinas.
  • Aproximación: para el recubrimiento de conjuntos (set cover), la regla voraz "elige el conjunto que cubra más elementos aún sin cubrir" no es óptima, pero tiene garantizado quedar dentro de un factor logarítmico del óptimo.
  • Problemas de entrevista: jump game, gas station, assign cookies, intervalos sin solapamiento, número mínimo de flechas para reventar globos, task scheduler.

Preguntas frecuentes

¿Qué es un algoritmo voraz, en palabras sencillas?

Es un algoritmo que siempre toma la opción que parece mejor en ese momento y nunca la reconsidera. Solo es correcto en problemas donde esas elecciones locales conducen siempre a la mejor respuesta global.

¿Cómo demuestro que un algoritmo voraz es correcto?

El método habitual es un argumento de intercambio: parte de cualquier solución óptima y muestra que puedes sustituir su primera elección por la elección voraz sin empeorarla; después repite el razonamiento con el problema restante.

¿Voraz o programación dinámica?

Prueba primero con un algoritmo voraz si una regla simple parece correcta y busca luego un contraejemplo. Si existe, el problema necesita programación dinámica, que compara todas las opciones de cada subproblema.

¿Es voraz el algoritmo de Dijkstra?

Sí. Siempre finaliza el nodo sin terminar más cercano. Esta decisión voraz es correcta porque los pesos de las aristas son no negativos.

Todos los tutoriales de programación