Apuntes DAM
Volver al inicio

Algoritmos voraces

AlgoritmosTécnicas de diseñoNivel intermedioTambién: greedy, algoritmos ávidos, algoritmos golosos

Construyen la solución paso a paso eligiendo siempre lo que parece mejor en ese momento, sin volver atrás. Son rápidos y sencillos, pero solo dan la solución óptima en algunos problemas.

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Algoritmo voraz: el cambio

Escribe las monedas que hay y la cantidad: el voraz coge siempre la moneda más grande que cabe. Prueba también con monedas raras (1 3 4 y 6) para verlo fallar.

De 1 a 10 valores distintos
  • en la zona de trabajo
  • fuera de juego

Paso 1

Hay que pagar 289. La regla voraz: coger siempre la moneda más grande que todavía cabe, sin mirar atrás ni pensar en el futuro.

1static List<Integer> cambio(int[] monedas, int cantidad) {   // monedas de mayor a menor
2    List<Integer> usadas = new ArrayList<>();
3    for (int m : monedas) {
4        while (cantidad >= m) {
5            usadas.add(m);
6            cantidad -= m;
7        }
8    }
9    return cantidad == 0 ? usadas : null;
10}

Variables

quedan
289
monedas
0

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Un algoritmo voraz (greedy) resuelve un problema tomando una decisión tras otra, y en cada paso elige la opción que parece mejor ahora mismo: la moneda más grande que cabe, la charla que termina antes, el objeto con más valor por kilo, la carretera más corta. Nunca deshace una elección ni mira qué pasará después.

Por eso son rápidos (casi siempre basta ordenar y recorrer una vez, O(n log n)) y fáciles de programar. El problema es que «lo mejor ahora» no siempre lleva a «lo mejor al final». Con las monedas de euro, coger siempre la más grande da el mínimo de monedas; pero con monedas de 4, 3 y 1, para pagar 6 el voraz coge 4 + 1 + 1 (tres monedas) cuando bastaban 3 + 3.

Que un voraz sea correcto hay que demostrarlo (normalmente, viendo que cualquier solución óptima se puede transformar en la voraz sin empeorarla). Hay problemas clásicos donde sí lo es: elegir el máximo de actividades que no se solapan (por hora de fin), la mochila en la que los objetos se pueden partir (por valor por kilo), Dijkstra para caminos mínimos, Kruskal y Prim para conectar una red con el mínimo coste, o los códigos de Huffman para comprimir.

Cuando el voraz no es óptimo, sigue siendo útil como aproximación rápida o como cota, y la respuesta exacta suele pedir programación dinámica o backtracking.

Cuándo usarlo

  • Cuando se puede demostrar (o es conocido) que la elección local lleva al óptimo: actividades, mochila fraccionaria, cambio con monedas de euro, Dijkstra, Kruskal, Huffman.
  • Cuando una buena solución rápida vale más que la óptima lenta (planificación aproximada, heurísticas).
  • Como primer intento o como cota para podar un backtracking.

Cuándo no

  • Si la elección de ahora puede estropear las de después y no hay demostración: mochila 0/1, cambio con monedas arbitrarias, el viajante. Ahí hace falta programación dinámica o backtracking.

Paso a paso

  1. El criterio. Decidir qué significa «lo mejor ahora»: la moneda mayor, la que termina antes, la de más valor por kilo… Es la decisión importante, y la que hay que justificar.
  2. Ordenar. Casi siempre se ordenan los candidatos por ese criterio (o se usa una cola de prioridad).
  3. Elegir y no mirar atrás. Se recorren los candidatos y se coge cada uno que sea compatible con lo ya elegido. Lo elegido no se cambia nunca.
  4. Comprobar. Al acabar, se comprueba si la solución está completa (¿se ha pagado todo?). Un voraz puede quedarse sin solución aunque exista.

El código

El cambio, con euros y con monedas raras

El mismo voraz da el óptimo con euros y falla con monedas de 4, 3 y 1.

Java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5    /** Cambio voraz: siempre la moneda más grande que cabe. monedas, de mayor a menor. */
6    static List<Integer> cambio(int[] monedas, int cantidad) {
7        List<Integer> usadas = new ArrayList<>();
8        for (int m : monedas) {
9            while (cantidad >= m) {               // mientras quepa, se coge (y no se replantea nunca)
10                usadas.add(m);
11                cantidad -= m;
12            }
13        }
14        return usadas;
15    }
16
17    public static void main(String[] args) {
18        int[] euros = {200, 100, 50, 20, 10, 5, 2, 1};
19        System.out.println("289 céntimos con euros: " + cambio(euros, 289));
20        int[] raras = {4, 3, 1};
21        System.out.println("6 con monedas de 4, 3 y 1: " + cambio(raras, 6) + ", pero bastan [3, 3]");
22    }
23}

Salida al ejecutarlo (la misma en los 5 lenguajes)

289 céntimos con euros: [200, 50, 20, 10, 5, 2, 2]
6 con monedas de 4, 3 y 1: [4, 1, 1], pero bastan [3, 3]

Elegir charlas que no se solapan

El voraz correcto: ordenar por hora de fin. Ordenar por hora de inicio o por duración no funciona (Spring empieza la primera y dura todo el día).

Java
1import java.util.*;
2
3public class Main {
4    record Charla(String nombre, int inicio, int fin) { }
5
6    /** El máximo de charlas sin solaparse: se ordena por hora de FIN y se coge cada una que empiece
7        cuando acaba la última elegida. Acabar pronto deja el máximo de hueco para las demás. */
8    static List<Charla> elegir(List<Charla> charlas) {
9        List<Charla> orden = new ArrayList<>(charlas);
10        orden.sort(Comparator.comparingInt(Charla::fin));
11        List<Charla> elegidas = new ArrayList<>();
12        int libreDesde = 0;
13        for (Charla c : orden) {
14            if (c.inicio() >= libreDesde) {
15                elegidas.add(c);
16                libreDesde = c.fin();
17            }
18        }
19        return elegidas;
20    }
21
22    public static void main(String[] args) {
23        List<Charla> charlas = List.of(
24            new Charla("Git", 9, 11), new Charla("Docker", 10, 12), new Charla("Java", 11, 13),
25            new Charla("SQL", 12, 14), new Charla("Kotlin", 13, 15), new Charla("Spring", 9, 15));
26        for (Charla c : elegir(charlas)) System.out.println(c.nombre() + " (" + c.inicio() + "-" + c.fin() + ")");
27    }
28}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Git (9-11)
Java (11-13)
Kotlin (13-15)

Traza: cambio de 289 céntimos con monedas de euro

MonedaCuántas se cogenQuedan
200189
100089
50139
20119
1019
514
220
100

Siete monedas (200 + 50 + 20 + 10 + 5 + 2 + 2): el mínimo posible, porque las monedas de euro son un sistema «canónico».

Complejidad

ProblemaCriterio voraz¿Óptimo?Coste
Cambio con monedas de euroLa moneda mayor que cabeSíO(monedas)
Cambio con monedas cualesquieraLa moneda mayor que cabeNo (hace falta dinámica)—
Máximo de actividades sin solaparseLa que termina antesSíO(n log n)
Mochila fraccionariaMás valor por kiloSíO(n log n)
Mochila 0/1 (no se parte)Más valor por kiloNo—
Camino mínimo (pesos ≥ 0)El nodo más cercano (Dijkstra)SíO((V + E) log V)

La rapidez viene de no volver atrás nunca: casi todo el coste está en ordenar.

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(n)
  • Mejor caso: O(n)
  • Caso medio: O(n)
  • Peor caso: O(n)

El cambio voraz recorre las monedas una vez (más una vuelta por moneda cogida). Ser rápido es su virtud; acertar, no siempre. Las curvas grises son las demás clases, para comparar.

En la práctica

  • Dijkstra (rutas del GPS), Prim y Kruskal (cableado o redes de mínimo coste) y Huffman (compresión ZIP y JPEG) son voraces.
  • Los planificadores de tareas y de procesos usan reglas voraces (la más corta primero, la de plazo más cercano).
  • Las cajas registradoras dan el cambio con el voraz porque el euro (y casi todas las monedas) es un sistema canónico.
  • Muchas heurísticas de optimización empiezan con una solución voraz y luego la mejoran.

Errores típicos

  • Dar por hecho que el voraz es óptimo sin comprobarlo: el error más común y más caro.
  • Elegir mal el criterio: en las actividades, ordenar por inicio o por duración da soluciones peores.
  • Olvidar ordenar antes de recorrer (o hacerlo al revés).
  • No comprobar al final si la solución está completa: con monedas de 5 y 2 no se pueden pagar 3, y el voraz devuelve algo incompleto.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Charlas sin solaparse

Cada línea es una charla de un congreso: nombre, hora de inicio y hora de fin (HH:MM). Elige el máximo número de charlas a las que se puede asistir sin que se solapen (una puede empezar justo a la hora en que acaba otra), con el criterio voraz correcto: por hora de fin. Con empate de fin, la que empieza antes; si también empatan, la que aparece antes en la entrada. Completa elegir.

  • Entrada: líneas como Git 09:00 11:00.
  • Salida: las elegidas por orden, 09:00-11:00 Git, y al final 3 de 6 charlas.
  • Errores: Charla no válida: «…» (horas mal escritas o fin no posterior al inicio) y No hay charlas.
☕JavaCharlas sin solaparseMedio

Ejemplo

Entrada (lo que se escribe por teclado)
Git 09:00 11:00
Docker 10:00 12:00
Java 11:00 13:00
SQL 12:00 14:00
Kotlin 13:00 15:00
Spring 09:00 15:00
Salida esperada
09:00-11:00 Git
11:00-13:00 Java
13:00-15:00 Kotlin
3 de 6 charlas
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    record Charla(String nombre, int inicio, int fin, int orden) { }
5
6    static String hora(int min) {
7        return String.format("%02d:%02d", min / 60, min % 60);
8    }
9
10    /** El máximo de charlas sin solaparse (una puede empezar justo cuando acaba otra): por hora de fin,
11        con empate por hora de inicio y luego por orden de la entrada. */
12    static List<Charla> elegir(List<Charla> charlas) {
13        List<Charla> orden = new ArrayList<>(charlas);
14        orden.sort(Comparator.comparingInt(Charla::fin).thenComparingInt(Charla::inicio).thenComparingInt(Charla::orden));
15        List<Charla> elegidas = new ArrayList<>();
16        int libre = 0;
17        for (Charla c : orden) {
18            if (c.inicio() >= libre) {
19                elegidas.add(c);
20                libre = c.fin();
21            }
22        }
23        return elegidas;
24    }
25
26    /** "HH:MM" en minutos, o -1. */
27    static int minutos(String t) {
28        if (!t.matches("\\d{2}:\\d{2}")) return -1;
29        int h = Integer.parseInt(t.substring(0, 2)), m = Integer.parseInt(t.substring(3));
30        return h < 24 && m < 60 ? h * 60 + m : -1;
31    }
32
33    public static void main(String[] args) {
34        Scanner sc = new Scanner(System.in);
35        List<Charla> charlas = new ArrayList<>();
36        while (sc.hasNextLine()) {
37            String linea = sc.nextLine().trim();
38            if (linea.isEmpty()) continue;
39            String[] p = linea.split("\\s+");
40            int ini = p.length == 3 ? minutos(p[1]) : -1, fin = p.length == 3 ? minutos(p[2]) : -1;
41            if (ini < 0 || fin <= ini) {
42                System.out.println("Charla no válida: «" + linea + "»");
43                continue;
44            }
45            charlas.add(new Charla(p[0], ini, fin, charlas.size()));
46        }
47        if (charlas.isEmpty()) {
48            System.out.println("No hay charlas");
49            return;
50        }
51        List<Charla> e = elegir(charlas);
52        for (Charla c : e) System.out.println(hora(c.inicio()) + "-" + hora(c.fin()) + " " + c.nombre());
53        System.out.println(e.size() + " de " + charlas.size() + " charlas");
54    }
55}

Elegir la que termina antes deja libre el máximo de tiempo para el resto: cualquier solución óptima se puede cambiar para que empiece por esa charla sin perder ninguna. Por eso este voraz es óptimo.

Ordenar es O(n log n) y el recorrido O(n): con mil charlas, inmediato. Probar todos los subconjuntos serían 2¹⁰⁰⁰ combinaciones.

2. La mochila fraccionaria

Un ladrón tiene una mochila que aguanta cierto peso y puede llevarse trozos de los objetos (son polvos, líquidos…). Para llevarse el máximo valor, el voraz coge primero lo que más vale por kilo, entero si cabe y, el último, partido. La primera línea es la capacidad en kg y cada línea siguiente un objeto: nombre, peso (kg) y valor (€). Con empate de valor por kilo, gana el primero de la entrada. Completa llenar.

  • Entrada: 50 y luego líneas como Oro 10 600.
  • Salida, en el orden en que se cogen: Oro: entero (10 kg) → 600,00 € o, el último si no cabe entero, Bronce: 10 de 20 kg → 50,00 €; al final, Valor total: 1010,00 €.
  • Errores: Capacidad no válida: «…» y Objeto no válido: «…» (peso de 1 a 99999 y valor de 0 a 999999, enteros).
☕JavaLa mochila fraccionariaMedio

Ejemplo

Entrada (lo que se escribe por teclado)
50
Oro 10 600
Plata 30 360
Bronce 20 100
Salida esperada
Oro: entero (10 kg) → 600,00 €
Plata: entero (30 kg) → 360,00 €
Bronce: 10 de 20 kg → 50,00 €
Valor total: 1010,00 €
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    record Objeto(String nombre, int peso, int valor, int orden) {
5        double porKilo() { return (double) valor / peso; }
6    }
7
8    /** Mochila fraccionaria: se cogen los objetos de mayor valor por kilo (con empate, el primero de la
9        entrada), enteros mientras quepan y el último partido. Devuelve los kilos que se cogen de cada
10        objeto, en el orden en que se cogen (solo los que tienen kilos). */
11    static LinkedHashMap<Objeto, Integer> llenar(List<Objeto> objetos, int capacidad) {
12        List<Objeto> orden = new ArrayList<>(objetos);
13        orden.sort(Comparator.comparingDouble(Objeto::porKilo).reversed().thenComparingInt(Objeto::orden));
14        LinkedHashMap<Objeto, Integer> r = new LinkedHashMap<>();
15        for (Objeto o : orden) {
16            if (capacidad == 0) break;
17            int kilos = Math.min(o.peso(), capacidad);
18            r.put(o, kilos);
19            capacidad -= kilos;
20        }
21        return r;
22    }
23
24    public static void main(String[] args) {
25        Scanner sc = new Scanner(System.in);
26        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
27        if (!primera.matches("[1-9]\\d{0,4}")) {
28            System.out.println("Capacidad no válida: «" + primera + "»");
29            return;
30        }
31        int capacidad = Integer.parseInt(primera);
32        List<Objeto> objetos = new ArrayList<>();
33        while (sc.hasNextLine()) {
34            String linea = sc.nextLine().trim();
35            if (linea.isEmpty()) continue;
36            String[] p = linea.split("\\s+");
37            if (p.length != 3 || !p[1].matches("[1-9]\\d{0,4}") || !p[2].matches("\\d{1,6}")) {
38                System.out.println("Objeto no válido: «" + linea + "»");
39                continue;
40            }
41            objetos.add(new Objeto(p[0], Integer.parseInt(p[1]), Integer.parseInt(p[2]), objetos.size()));
42        }
43        Locale es = Locale.forLanguageTag("es-ES");
44        double total = 0;
45        for (Map.Entry<Objeto, Integer> e : llenar(objetos, capacidad).entrySet()) {
46            Objeto o = e.getKey();
47            int kg = e.getValue();
48            double valor = (double) o.valor() * kg / o.peso();
49            total += valor;
50            System.out.println(o.nombre() + ": " + (kg == o.peso() ? "entero (" + kg + " kg)" : kg + " de " + o.peso() + " kg") + String.format(es, " → %.2f €", valor));
51        }
52        System.out.println(String.format(es, "Valor total: %.2f €", total));
53    }
54}

Como los objetos se pueden partir, cada kilo de mochila debe ir al objeto que más paga por kilo: cambiar un kilo de algo peor por uno de algo mejor nunca empeora. Por eso el voraz es óptimo.

Si los objetos no se pudieran partir (mochila 0/1), este mismo voraz fallaría: ahí hace falta programación dinámica.

Test

Test: Algoritmos voraces

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Qué caracteriza a un algoritmo voraz?

  2. 2.Con monedas de 4, 3 y 1, ¿cuántas monedas usa el voraz para pagar 6?

  3. 3.Para elegir el máximo de actividades que no se solapan, ¿por qué criterio se ordenan?

  4. 4.¿En cuál de estos problemas el voraz por valor/peso NO da el óptimo?

  5. 5.¿Qué algoritmo de grafos es voraz?

Relacionado