Algoritmos voraces
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.
- 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
- 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.
- Ordenar. Casi siempre se ordenan los candidatos por ese criterio (o se usa una cola de prioridad).
- 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.
- 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.
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}def cambio(monedas, cantidad):
"""Cambio voraz: siempre la moneda más grande que cabe. monedas, de mayor a menor."""
usadas = []
for m in monedas:
while cantidad >= m: # mientras quepa, se coge (y no se replantea nunca)
usadas.append(m)
cantidad -= m
return usadas
euros = [200, 100, 50, 20, 10, 5, 2, 1]
print("289 céntimos con euros:", cambio(euros, 289))
raras = [4, 3, 1]
print(f"6 con monedas de 4, 3 y 1: {cambio(raras, 6)}, pero bastan [3, 3]")/** Cambio voraz: siempre la moneda más grande que cabe. monedas, de mayor a menor. */
function cambio(monedas, cantidad) {
const usadas = [];
for (const m of monedas) {
while (cantidad >= m) { // mientras quepa, se coge (y no se replantea nunca)
usadas.push(m);
cantidad -= m;
}
}
return usadas;
}
const texto = (a) => "[" + a.join(", ") + "]";
const euros = [200, 100, 50, 20, 10, 5, 2, 1];
console.log("289 céntimos con euros: " + texto(cambio(euros, 289)));
const raras = [4, 3, 1];
console.log("6 con monedas de 4, 3 y 1: " + texto(cambio(raras, 6)) + ", pero bastan [3, 3]");using System;
using System.Collections.Generic;
class Program {
// Cambio voraz: siempre la moneda más grande que cabe. monedas, de mayor a menor.
static List<int> Cambio(int[] monedas, int cantidad) {
var usadas = new List<int>();
foreach (int m in monedas) {
while (cantidad >= m) { // mientras quepa, se coge (y no se replantea nunca)
usadas.Add(m);
cantidad -= m;
}
}
return usadas;
}
static string Texto(List<int> l) => "[" + string.Join(", ", l) + "]";
static void Main() {
int[] euros = { 200, 100, 50, 20, 10, 5, 2, 1 };
Console.WriteLine("289 céntimos con euros: " + Texto(Cambio(euros, 289)));
int[] raras = { 4, 3, 1 };
Console.WriteLine("6 con monedas de 4, 3 y 1: " + Texto(Cambio(raras, 6)) + ", pero bastan [3, 3]");
}
}<?php
/** Cambio voraz: siempre la moneda más grande que cabe. $monedas, de mayor a menor. */
function cambio(array $monedas, int $cantidad): array {
$usadas = [];
foreach ($monedas as $m) {
while ($cantidad >= $m) { // mientras quepa, se coge (y no se replantea nunca)
$usadas[] = $m;
$cantidad -= $m;
}
}
return $usadas;
}
function texto(array $a): string { return "[" . implode(", ", $a) . "]"; }
$euros = [200, 100, 50, 20, 10, 5, 2, 1];
echo "289 céntimos con euros: " . texto(cambio($euros, 289)) . "\n";
$raras = [4, 3, 1];
echo "6 con monedas de 4, 3 y 1: " . texto(cambio($raras, 6)) . ", pero bastan [3, 3]\n";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).
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}from collections import namedtuple
Charla = namedtuple("Charla", "nombre inicio fin")
def elegir(charlas):
"""El máximo de charlas sin solaparse: se ordena por hora de FIN y se coge cada una que empiece
cuando acaba la última elegida. Acabar pronto deja el máximo de hueco para las demás."""
elegidas, libre_desde = [], 0
for c in sorted(charlas, key=lambda c: c.fin):
if c.inicio >= libre_desde:
elegidas.append(c)
libre_desde = c.fin
return elegidas
charlas = [Charla("Git", 9, 11), Charla("Docker", 10, 12), Charla("Java", 11, 13),
Charla("SQL", 12, 14), Charla("Kotlin", 13, 15), Charla("Spring", 9, 15)]
for c in elegir(charlas):
print(f"{c.nombre} ({c.inicio}-{c.fin})")/** El máximo de charlas sin solaparse: se ordena por hora de FIN y se coge cada una que empiece
cuando acaba la última elegida. Acabar pronto deja el máximo de hueco para las demás. */
function elegir(charlas) {
const elegidas = [];
let libreDesde = 0;
for (const c of [...charlas].sort((a, b) => a.fin - b.fin)) {
if (c.inicio >= libreDesde) {
elegidas.push(c);
libreDesde = c.fin;
}
}
return elegidas;
}
const charlas = [
{ nombre: "Git", inicio: 9, fin: 11 }, { nombre: "Docker", inicio: 10, fin: 12 }, { nombre: "Java", inicio: 11, fin: 13 },
{ nombre: "SQL", inicio: 12, fin: 14 }, { nombre: "Kotlin", inicio: 13, fin: 15 }, { nombre: "Spring", inicio: 9, fin: 15 },
];
for (const c of elegir(charlas)) console.log(`${c.nombre} (${c.inicio}-${c.fin})`);using System;
using System.Collections.Generic;
using System.Linq;
record Charla(string Nombre, int Inicio, int Fin);
class Program {
// El máximo de charlas sin solaparse: se ordena por hora de FIN y se coge cada una que empiece
// cuando acaba la última elegida. Acabar pronto deja el máximo de hueco para las demás.
static List<Charla> Elegir(IEnumerable<Charla> charlas) {
var elegidas = new List<Charla>();
int libreDesde = 0;
foreach (var c in charlas.OrderBy(c => c.Fin)) {
if (c.Inicio >= libreDesde) {
elegidas.Add(c);
libreDesde = c.Fin;
}
}
return elegidas;
}
static void Main() {
var charlas = new List<Charla> {
new("Git", 9, 11), new("Docker", 10, 12), new("Java", 11, 13),
new("SQL", 12, 14), new("Kotlin", 13, 15), new("Spring", 9, 15),
};
foreach (var c in Elegir(charlas)) Console.WriteLine(quot;{c.Nombre} ({c.Inicio}-{c.Fin})");
}
}<?php
/** El máximo de charlas sin solaparse: se ordena por hora de FIN y se coge cada una que empiece
cuando acaba la última elegida. Acabar pronto deja el máximo de hueco para las demás. */
function elegir(array $charlas): array {
usort($charlas, fn($a, $b) => $a["fin"] <=> $b["fin"]);
$elegidas = [];
$libreDesde = 0;
foreach ($charlas as $c) {
if ($c["inicio"] >= $libreDesde) {
$elegidas[] = $c;
$libreDesde = $c["fin"];
}
}
return $elegidas;
}
$charlas = [
["nombre" => "Git", "inicio" => 9, "fin" => 11], ["nombre" => "Docker", "inicio" => 10, "fin" => 12],
["nombre" => "Java", "inicio" => 11, "fin" => 13], ["nombre" => "SQL", "inicio" => 12, "fin" => 14],
["nombre" => "Kotlin", "inicio" => 13, "fin" => 15], ["nombre" => "Spring", "inicio" => 9, "fin" => 15],
];
foreach (elegir($charlas) as $c) echo "{$c['nombre']} ({$c['inicio']}-{$c['fin']})\n";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
| Moneda | Cuántas se cogen | Quedan |
|---|---|---|
| 200 | 1 | 89 |
| 100 | 0 | 89 |
| 50 | 1 | 39 |
| 20 | 1 | 19 |
| 10 | 1 | 9 |
| 5 | 1 | 4 |
| 2 | 2 | 0 |
| 1 | 0 | 0 |
Siete monedas (200 + 50 + 20 + 10 + 5 + 2 + 2): el mínimo posible, porque las monedas de euro son un sistema «canónico».
Complejidad
| Problema | Criterio voraz | ¿Óptimo? | Coste |
|---|---|---|---|
| Cambio con monedas de euro | La moneda mayor que cabe | Sí | O(monedas) |
| Cambio con monedas cualesquiera | La moneda mayor que cabe | No (hace falta dinámica) | — |
| Máximo de actividades sin solaparse | La que termina antes | Sí | O(n log n) |
| Mochila fraccionaria | Más valor por kilo | Sí | O(n log n) |
| Mochila 0/1 (no se parte) | Más valor por kilo | No | — |
| 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.
- 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 final3 de 6 charlas. - Errores:
Charla no válida: «…»(horas mal escritas o fin no posterior al inicio) yNo hay charlas.
Ejemplo
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
09:00-11:00 Git 11:00-13:00 Java 13:00-15:00 Kotlin 3 de 6 charlas
Ver la solución explicada
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:
50y luego líneas comoOro 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: «…»yObjeto no válido: «…»(peso de 1 a 99999 y valor de 0 a 999999, enteros).
Ejemplo
50 Oro 10 600 Plata 30 360 Bronce 20 100
Oro: entero (10 kg) → 600,00 € Plata: entero (30 kg) → 360,00 € Bronce: 10 de 20 kg → 50,00 € Valor total: 1010,00 €
Ver la solución explicada
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 aciertosElige 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.¿Qué caracteriza a un algoritmo voraz?
2.Con monedas de 4, 3 y 1, ¿cuántas monedas usa el voraz para pagar 6?
3.Para elegir el máximo de actividades que no se solapan, ¿por qué criterio se ordenan?
4.¿En cuál de estos problemas el voraz por valor/peso NO da el óptimo?
5.¿Qué algoritmo de grafos es voraz?