TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Problema estilo CTCI 6.5 para principiantes: dos jarras de capacidad 3 y 5 litros, medir exactamente 4. Pasos manuales de llenado, identidad de Bézout y BFS opcional en Java sobre estados.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
Tienes una jarra de 3 litros y otra de 5 litros. Sin otras marcas. Un lago (o grifo) ilimitado para llenar, y puedes vaciar cualquiera de las dos. ¿Puedes acabar con exactamente 4 litros en una de ellas?
Este es el puzzle clásico de las jarras de agua. También es teoría de números en la cocina: las cantidades que puedes medir son múltiplos de gcd(3, 5), que es 1, así que 4 es posible. En entrevista quieren los pasos, el porqué y a veces un programa de búsqueda que los encuentre.
Este post es enseñanza original para principiantes en Java. Misma familia que los puzzles clásicos de jarras y acertijos al estilo Die Hard, no una copia de libro. Parte de la serie CTCI en Java. Capítulo 6, puzzles de matemáticas y lógica.
1. Analogía cotidiana
Piensa en dos vasos medidores sin rayas de medio litro. Uno cabe tres vasos de agua, el otro cinco. Puedes:
- Llenar un vaso del todo desde el grifo.
- Vaciar un vaso del todo.
- Verter de uno al otro hasta que el origen se vacíe o el destino se llene.
Nunca llenas "a ojo" a medias. Cada cantidad que creas sale de llenados completos, vaciados completos y trasvases que paran en la capacidad. El puzzle es si 4 litros aparecen como contenido de una jarra tras una secuencia corta de esos movimientos.
2. Problema en palabras simples
Dados:
- Capacidad de la jarra A: 3 litros.
- Capacidad de la jarra B: 5 litros.
- Fuente de agua ilimitada y espacio para vaciar (puedes vaciar cualquiera por completo).
Operaciones permitidas:
- Llenar A del todo desde la fuente.
- Llenar B del todo desde la fuente.
- Vaciar A por completo.
- Vaciar B por completo.
- Verter A en B hasta que A quede vacía o B llena.
- Verter B en A hasta que B quede vacía o A llena.
Objetivo: llegar a cualquier estado donde A o B (o ambas) tenga exactamente 4 litros. En el enunciado habitual, los 4 quedan en la jarra de 5 litros.
Aclara antes de codificar o escribir pasos:
- ¿Los 4 deben estar en una sola jarra? (Sí en esta versión clásica.)
- ¿Hay un tercer recipiente? (No.)
- ¿Empiezan vacías? (Sí.)
- ¿Solo litros enteros? (Sí: cantidades enteras.)
- Caso general después: capacidades
m,n, objetivod. Mismas ideas.
3. Piensa primero
Qué puedes medir de verdad
Cada operación:
- Añade una capacidad completa (
+3o+5desde la fuente, al llenar), - Resta una capacidad completa (al vaciar),
- O mueve agua entre jarras sin cambiar el total de agua que hay ahora.
Si solo te importan cantidades que aparecen en una jarra, son combinaciones lineales enteras de 3 y 5:
a*3 + b*5 para algunos enteros a, b (positivos o negativos)
Negativo significa "vaciar tantas veces" en la contabilidad clásica. Identidad de Bézout: el conjunto de todas esas combinaciones es exactamente los múltiplos de gcd(3, 5) = 1. Así puedes medir 1, 2, 3, 4 o 5 litros en una de las jarras (respetando la capacidad). No puedes medir 4 con jarras de 6 y 9, porque gcd(6, 9) = 3 y 3 no divide a 4.
Regla del puzzle general: el objetivo d es resoluble si y solo si d es múltiplo de gcd(m, n) y 0 < d <= max(m, n) (para "exactamente d en una jarra").
Estados, no magia
Un estado es un par (a, b): litros en la jarra de 3 y litros en la de 5.
- Inicio:
(0, 0). - Meta: cualquier estado con
a == 4ob == 4. Aquí solo B puede contener 4, así queb == 4.
Desde cualquier estado hay como mucho seis movimientos. El grafo es minúsculo: 4 valores posibles para A (0..3) por 6 para B (0..5) = 24 estados. Búsqueda en anchura encuentra una secuencia más corta si quieres código. En la pizarra, un camino manual corto basta.
Un camino manual limpio (6 operaciones)
Sigue (jarra-3, jarra-5):
(0, 0) inicio
(0, 5) llena la de 5
(3, 2) vierte la de 5 en la de 3 hasta llenar la de 3; quedan 2 en la de 5
(0, 2) vacía la de 3
(2, 0) vierte los 2 restantes en la de 3
(2, 5) llena otra vez la de 5
(3, 4) vierte la de 5 en la de 3 hasta llenarla (le falta 1); quedan 4 en la de 5
Listo. La jarra de 5 litros tiene exactamente 4 litros.
Otro camino (empezar con la de 3)
(0, 0)
(3, 0) llena 3
(0, 3) vierte en 5
(3, 3) llena 3
(1, 5) vierte en 5 hasta llenar; queda 1 en la de 3
(1, 0) vacía 5
(0, 1) vierte el 1 en 5
(3, 1) llena 3
(0, 4) vierte en 5; la de 5 tiene 1+3 = 4
Más largo, misma idea: construyes residuos de 3 módulo 5 (o al revés).
4. Soluciones en Java
(a) Documentar la receta manual (lo que suele querer la entrevista primero)
// Manual sequence for (3, 5) -> 4 in the five-liter jug.
// States written as (small, large).
//
// (0,0) fill large -> (0,5)
// pour large->small -> (3,2)
// empty small -> (0,2)
// pour large->small -> (2,0)
// fill large -> (2,5)
// pour large->small -> (3,4) // large has 4
Dilo en voz alta y luego escribe la comprobación de Bézout para que vean que no adivinas.
(b) Ayuda de resolubilidad (general m, n, d)
static int gcd(int x, int y) {
x = Math.abs(x);
y = Math.abs(y);
while (y != 0) {
int t = x % y;
x = y;
y = t;
}
return x;
}
/** True if you can obtain exactly d liters in one jug of capacities m and n. */
static boolean canMeasure(int m, int n, int d) {
if (d == 0) {
return true; // both empty
}
if (m + n < d) {
return false;
}
// Exactly d in one jug: d must fit in at least one jug
if (d > m && d > n) {
return false;
}
return d % gcd(m, n) == 0;
}
Para m = 3, n = 5, d = 4: gcd es 1, 4 cabe en la de 5, así que true.
(c) Opcional: BFS sobre estados (lista de pasos más corta)
Útil cuando las capacidades son mayores o piden un programa. El espacio de estados es (m+1)*(n+1).
import java.util.*;
public class WaterJugs {
record State(int a, int b) {}
static List<String> measure(int m, int n, int d) {
if (!canMeasure(m, n, d) && d != 0) {
return List.of(); // impossible
}
if (d == 0) {
return List.of("start (0,0)");
}
Queue<State> q = new ArrayDeque<>();
Map<State, String> how = new HashMap<>(); // state -> last move label
Map<State, State> prev = new HashMap<>();
State start = new State(0, 0);
q.add(start);
how.put(start, "start");
prev.put(start, null);
while (!q.isEmpty()) {
State cur = q.poll();
if (cur.a == d || cur.b == d || cur.a + cur.b == d) {
// classic "in one jug": prefer a==d or b==d
if (cur.a == d || cur.b == d) {
return reconstruct(cur, prev, how);
}
}
for (Object[] step : neighbors(cur, m, n)) {
State nxt = (State) step[0];
String label = (String) step[1];
if (how.containsKey(nxt)) {
continue;
}
how.put(nxt, label);
prev.put(nxt, cur);
q.add(nxt);
}
}
return List.of(); // unreachable (should not happen if canMeasure)
}
static List<Object[]> neighbors(State s, int m, int n) {
int a = s.a, b = s.b;
List<Object[]> out = new ArrayList<>();
out.add(new Object[]{new State(m, b), "fill A"});
out.add(new Object[]{new State(a, n), "fill B"});
out.add(new Object[]{new State(0, b), "empty A"});
out.add(new Object[]{new State(a, 0), "empty B"});
// pour A -> B
int pourAB = Math.min(a, n - b);
out.add(new Object[]{new State(a - pourAB, b + pourAB), "pour A->B"});
// pour B -> A
int pourBA = Math.min(b, m - a);
out.add(new Object[]{new State(a + pourBA, b - pourBA), "pour B->A"});
return out;
}
static List<String> reconstruct(State end, Map<State, State> prev, Map<State, String> how) {
LinkedList<String> path = new LinkedList<>();
State cur = end;
while (cur != null) {
path.addFirst(how.get(cur) + " -> (" + cur.a + "," + cur.b + ")");
cur = prev.get(cur);
}
return path;
}
static boolean canMeasure(int m, int n, int d) {
if (d == 0) return true;
if (d > m && d > n) return false;
if (m + n < d) return false;
return d % gcd(m, n) == 0;
}
static int gcd(int x, int y) {
x = Math.abs(x);
y = Math.abs(y);
while (y != 0) {
int t = x % y;
x = y;
y = t;
}
return x;
}
public static void main(String[] args) {
System.out.println(canMeasure(3, 5, 4)); // true
for (String step : measure(3, 5, 4)) {
System.out.println(step);
}
}
}
BFS devuelve un camino más corto. El de seis pasos de arriba es longitud mínima para 4 litros; el más largo "empezar con 3" es válido pero no mínimo.
Recorrido de la matemática del trasvase
Cuando viertes A en B:
spaceInB = n - b
moved = min(a, spaceInB)
newA = a - moved
newB = b + moved
Misma idea al revés. Eso es toda la simulación. Sin floats.
5. Tabla de complejidad
| Enfoque | Tiempo | Espacio extra | Notas |
|---|---|---|---|
| Receta manual de 6 pasos | O(1) | O(1) | Vale para 3 y 5 fijos |
| Comprobación Bézout / gcd | O(log min(m,n)) | O(1) | Solo resolubilidad, no los pasos |
| BFS sobre estados | O(m * n) | O(m * n) | Secuencia más corta de operaciones |
| DFS / recursión | mismo orden | pila peores casos | Prefiere BFS para camino más corto |
Para tamaños de entrevista como 3 y 5, dominan las constantes. El test de gcd es la herramienta teórica limpia.
6. Casos límite
- Objetivo 0 → ya resuelto: ambas vacías.
- Objetivo igual a una capacidad → un solo llenado. Ejemplo: objetivo 3 con jarra de 3.
- Objetivo mayor que ambas jarras → imposible si necesitas
den una sola jarra. gcdno divide ad→ imposible. Ejemplo: 4 con jarras 6 y 9.- Una capacidad 0 → solo múltiplos de la otra (normalmente 0 y esa capacidad).
- Misma capacidad → solo mides 0 o esa capacidad (para una jarra).
- Preferir 4 en la grande → comprueba solo
b == 4, o acepta cualquiera. - No inventes medios litros → todo se queda en enteros.
- Estilo LeetCode 365 → "puedes medir" solo necesita gcd; "imprime pasos" necesita BFS o una construcción explícita.
Comprobaciones mínimas:
assert canMeasure(3, 5, 4);
assert canMeasure(3, 5, 1);
assert !canMeasure(2, 6, 5);
assert canMeasure(3, 5, 0);
7. Explícaselo a un amigo
Jarras de agua pregunta: con solo llenados completos, vaciados completos y trasvases entre una de 3 y una de 5, ¿puedes obtener exactamente 4 litros?
- Los movimientos solo producen combinaciones enteras de 3 y 5.
- Esas combinaciones son múltiplos de
gcd(3, 5) = 1, así que 4 es posible y cabe en la jarra de 5. - Una receta corta: llena 5, vierte en 3, vacía 3, vierte los 2 restantes en 3, llena 5, vierte en 3 hasta llenar. En la de 5 quedan 4.
- En código, modela estados
(a, b)y haz BFS de las seis operaciones si necesitas el camino en automático. - En general: resoluble cuando
d % gcd(m, n) == 0ydcabe en al menos una jarra.
Si puedes recorrer la tabla (0,0) ... (3,4) en la pizarra, decir "Bézout" sin bloquearte y esbozar un BFS de 24 estados, dominas el problema 6.5.
Serie
- Guía: Guía de la serie CTCI
- Anterior: Hormigas en un triángulo
- Siguiente: Isla de ojos azules
