Programación dinámica
Resuelve un problema a partir de sus subproblemas guardando cada solución para no calcularla dos veces. Convierte recursividades exponenciales en tablas que se rellenan en tiempo polinómico.
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.
Programación dinámica: el cambio óptimo
Escribe las monedas y la cantidad: dp[v] guarda el mínimo de monedas para pagar v, y cada casilla se calcula con las anteriores.
- en la zona de trabajo
- la que se calcula
- fuera de juego
Paso 1
dp[0] = 0: pagar 0 no necesita ninguna moneda. Todas las demás empiezan en ∞ (aún no se sabe pagarlas).
1static int minMonedas(int[] monedas, int cantidad) {
2 int[] dp = new int[cantidad + 1]; // dp[v] = mínimo de monedas para pagar v
3 Arrays.fill(dp, Integer.MAX_VALUE);
4 dp[0] = 0; // v = 0, dp[0] = 0
5 for (int v = 1; v <= cantidad; v++)
6 for (int m : monedas)
7 if (m <= v && dp[v - m] != Integer.MAX_VALUE)
8 dp[v] = Math.min(dp[v], dp[v - m] + 1);
9 return dp[cantidad] == Integer.MAX_VALUE ? -1 : dp[cantidad];
10}Variables
- v
- 0
- dp[0]
- 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
Algunos problemas se resuelven bien con recursividad pero repiten muchísimo trabajo: el Fibonacci ingenuo calcula fib(30) más de un millón de veces la misma fib(2). La programación dinámica se basa en una idea sencilla: si un subproblema ya se ha resuelto, se guarda su resultado y la próxima vez se consulta en vez de recalcularlo.
Funciona cuando el problema cumple dos condiciones. Subestructura óptima: la mejor solución del problema se construye con las mejores soluciones de sus subproblemas (el mejor cambio para 6 es una moneda más el mejor cambio para lo que queda). Y subproblemas solapados: los mismos subproblemas aparecen una y otra vez (si no se repitieran, sería divide y vencerás).
Se puede escribir de dos formas. De arriba abajo (memoización): la recursividad natural, más un mapa o un array donde se apunta cada resultado. De abajo arriba (tabulación): se rellena una tabla empezando por los casos más pequeños, de modo que cuando se calcula una casilla, las que necesita ya están. La tabulación evita la recursividad y suele permitir ahorrar memoria.
La parte difícil no es programarla sino plantearla: decidir qué significa cada casilla («dp[v] = mínimo de monedas para pagar v»), cuál es la recurrencia que la relaciona con las anteriores y cuáles son los casos base. Una vez escrito eso en una frase, el código sale casi solo.
Cuándo usarlo
- Problemas de optimización o de contar formas en los que la solución se construye con soluciones de subproblemas que se repiten: cambio de monedas, mochila 0/1, caminos en una cuadrícula, subir escaleras.
- Comparar secuencias: distancia de edición (correctores, diff), subsecuencia común más larga, alineamiento de ADN.
- Cuando un voraz no da el óptimo y probar todas las combinaciones es exponencial.
Cuándo no
- Si los subproblemas no se repiten: basta con divide y vencerás (o recursividad simple).
- Si existe un voraz demostrado óptimo: es más simple y más rápido.
- Si la tabla es demasiado grande (la mochila con una capacidad de miles de millones).
Paso a paso
- Definir el estado. Qué guarda cada casilla, en una frase: «dp[v] es el mínimo de monedas para pagar exactamente v».
- La recurrencia. Cómo se calcula una casilla con otras más pequeñas:
dp[v] = min(dp[v − m] + 1)para cada moneda m que cabe. - Los casos base. Las casillas que se saben sin calcular:
dp[0] = 0. - El orden. Rellenar la tabla en un orden en que las casillas necesarias ya estén calculadas (de 0 hacia arriba), o memoizar la recursividad. Si hace falta la solución y no solo su valor, se apunta en cada casilla qué decisión se tomó y se reconstruye al final.
El código
Subir una escalera: memoización y tabulación
Las dos formas de la programación dinámica para el mismo problema (que es Fibonacci disfrazado).
1import java.util.Arrays;
2
3public class Main {
4 static long[] memo;
5
6 /** De arriba abajo (memoización): la recursividad de siempre, apuntando cada resultado. */
7 static long formas(int n) {
8 if (n <= 1) return 1; // 0 o 1 escalón: una sola forma
9 if (memo[n] != 0) return memo[n];
10 return memo[n] = formas(n - 1) + formas(n - 2); // el último paso fue de 1 o de 2
11 }
12
13 /** De abajo arriba (tabulación): se rellena la tabla desde los casos pequeños, sin recursividad. */
14 static long[] tabla(int n) {
15 long[] dp = new long[n + 1];
16 dp[0] = 1;
17 dp[1] = 1;
18 for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
19 return dp;
20 }
21
22 public static void main(String[] args) {
23 memo = new long[11];
24 System.out.println("Formas de subir 10 escalones de 1 en 1 o de 2 en 2: " + formas(10));
25 System.out.println("La tabla, de 0 a 10 escalones: " + Arrays.toString(tabla(10)));
26 }
27}from functools import cache
@cache # de arriba abajo: Python apunta cada resultado solo
def formas(n):
if n <= 1:
return 1 # 0 o 1 escalón: una sola forma
return formas(n - 1) + formas(n - 2) # el último paso fue de 1 o de 2
def tabla(n):
"""De abajo arriba (tabulación): se rellena la tabla desde los casos pequeños, sin recursividad."""
dp = [1, 1] + [0] * (n - 1)
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp
print("Formas de subir 10 escalones de 1 en 1 o de 2 en 2:", formas(10))
print("La tabla, de 0 a 10 escalones:", tabla(10))const memo = new Map();
/** De arriba abajo (memoización): la recursividad de siempre, apuntando cada resultado. */
function formas(n) {
if (n <= 1) return 1; // 0 o 1 escalón: una sola forma
if (memo.has(n)) return memo.get(n);
const r = formas(n - 1) + formas(n - 2); // el último paso fue de 1 o de 2
memo.set(n, r);
return r;
}
/** De abajo arriba (tabulación): se rellena la tabla desde los casos pequeños, sin recursividad. */
function tabla(n) {
const dp = [1, 1];
for (let i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
return dp;
}
console.log("Formas de subir 10 escalones de 1 en 1 o de 2 en 2: " + formas(10));
console.log("La tabla, de 0 a 10 escalones: [" + tabla(10).join(", ") + "]");using System;
class Program {
static readonly long[] memo = new long[11];
// De arriba abajo (memoización): la recursividad de siempre, apuntando cada resultado.
static long Formas(int n) {
if (n <= 1) return 1; // 0 o 1 escalón: una sola forma
if (memo[n] != 0) return memo[n];
return memo[n] = Formas(n - 1) + Formas(n - 2); // el último paso fue de 1 o de 2
}
// De abajo arriba (tabulación): se rellena la tabla desde los casos pequeños, sin recursividad.
static long[] Tabla(int n) {
var dp = new long[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
return dp;
}
static void Main() {
Console.WriteLine("Formas de subir 10 escalones de 1 en 1 o de 2 en 2: " + Formas(10));
Console.WriteLine("La tabla, de 0 a 10 escalones: [" + string.Join(", ", Tabla(10)) + "]");
}
}<?php
$memo = [];
/** De arriba abajo (memoización): la recursividad de siempre, apuntando cada resultado. */
function formas(int $n): int {
global $memo;
if ($n <= 1) return 1; // 0 o 1 escalón: una sola forma
return $memo[$n] ??= formas($n - 1) + formas($n - 2); // el último paso fue de 1 o de 2
}
/** De abajo arriba (tabulación): se rellena la tabla desde los casos pequeños, sin recursividad. */
function tabla(int $n): array {
$dp = [1, 1];
for ($i = 2; $i <= $n; $i++) $dp[$i] = $dp[$i - 1] + $dp[$i - 2];
return $dp;
}
echo "Formas de subir 10 escalones de 1 en 1 o de 2 en 2: " . formas(10) . "\n";
echo "La tabla, de 0 a 10 escalones: [" . implode(", ", tabla(10)) . "]\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Formas de subir 10 escalones de 1 en 1 o de 2 en 2: 89 La tabla, de 0 a 10 escalones: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
El cambio óptimo, reconstruyendo las monedas
Además del mínimo, se apunta qué moneda mejoró cada casilla: recorriendo esos apuntes desde el final sale la solución.
1import java.util.*;
2
3public class Main {
4 /** El mínimo de monedas para pagar cantidad, y cuáles. dp[v] = mínimo para pagar v. */
5 static String cambio(int[] monedas, int cantidad) {
6 int[] dp = new int[cantidad + 1], ultima = new int[cantidad + 1];
7 Arrays.fill(dp, Integer.MAX_VALUE);
8 dp[0] = 0;
9 for (int v = 1; v <= cantidad; v++)
10 for (int m : monedas)
11 if (m <= v && dp[v - m] != Integer.MAX_VALUE && dp[v - m] + 1 < dp[v]) {
12 dp[v] = dp[v - m] + 1;
13 ultima[v] = m; // se apunta la moneda para poder reconstruir
14 }
15 if (dp[cantidad] == Integer.MAX_VALUE) return cantidad + ": imposible";
16 StringJoiner sj = new StringJoiner(" + ");
17 for (int v = cantidad; v > 0; v -= ultima[v]) sj.add(String.valueOf(ultima[v]));
18 return cantidad + " = " + sj + " (" + dp[cantidad] + " monedas)";
19 }
20
21 public static void main(String[] args) {
22 System.out.println(cambio(new int[] {1, 3, 4}, 6));
23 System.out.println(cambio(new int[] {1, 5, 10, 25}, 63));
24 System.out.println(cambio(new int[] {5, 2}, 3));
25 }
26}def cambio(monedas, cantidad):
"""El mínimo de monedas para pagar cantidad, y cuáles. dp[v] = mínimo para pagar v."""
INF = float("inf")
dp = [0] + [INF] * cantidad
ultima = [0] * (cantidad + 1)
for v in range(1, cantidad + 1):
for m in monedas:
if m <= v and dp[v - m] + 1 < dp[v]:
dp[v] = dp[v - m] + 1
ultima[v] = m # se apunta la moneda para poder reconstruir
if dp[cantidad] == INF:
return f"{cantidad}: imposible"
usadas, v = [], cantidad
while v > 0:
usadas.append(str(ultima[v]))
v -= ultima[v]
return f"{cantidad} = {' + '.join(usadas)} ({dp[cantidad]} monedas)"
print(cambio([1, 3, 4], 6))
print(cambio([1, 5, 10, 25], 63))
print(cambio([5, 2], 3))/** El mínimo de monedas para pagar cantidad, y cuáles. dp[v] = mínimo para pagar v. */
function cambio(monedas, cantidad) {
const dp = new Array(cantidad + 1).fill(Infinity), ultima = new Array(cantidad + 1).fill(0);
dp[0] = 0;
for (let v = 1; v <= cantidad; v++)
for (const m of monedas)
if (m <= v && dp[v - m] + 1 < dp[v]) {
dp[v] = dp[v - m] + 1;
ultima[v] = m; // se apunta la moneda para poder reconstruir
}
if (dp[cantidad] === Infinity) return `${cantidad}: imposible`;
const usadas = [];
for (let v = cantidad; v > 0; v -= ultima[v]) usadas.push(ultima[v]);
return `${cantidad} = ${usadas.join(" + ")} (${dp[cantidad]} monedas)`;
}
console.log(cambio([1, 3, 4], 6));
console.log(cambio([1, 5, 10, 25], 63));
console.log(cambio([5, 2], 3));using System;
using System.Collections.Generic;
class Program {
// El mínimo de monedas para pagar cantidad, y cuáles. dp[v] = mínimo para pagar v.
static string Cambio(int[] monedas, int cantidad) {
int[] dp = new int[cantidad + 1], ultima = new int[cantidad + 1];
Array.Fill(dp, int.MaxValue);
dp[0] = 0;
for (int v = 1; v <= cantidad; v++)
foreach (int m in monedas)
if (m <= v && dp[v - m] != int.MaxValue && dp[v - m] + 1 < dp[v]) {
dp[v] = dp[v - m] + 1;
ultima[v] = m; // se apunta la moneda para poder reconstruir
}
if (dp[cantidad] == int.MaxValue) return quot;{cantidad}: imposible";
var usadas = new List<int>();
for (int v = cantidad; v > 0; v -= ultima[v]) usadas.Add(ultima[v]);
return quot;{cantidad} = {string.Join(" + ", usadas)} ({dp[cantidad]} monedas)";
}
static void Main() {
Console.WriteLine(Cambio(new[] { 1, 3, 4 }, 6));
Console.WriteLine(Cambio(new[] { 1, 5, 10, 25 }, 63));
Console.WriteLine(Cambio(new[] { 5, 2 }, 3));
}
}<?php
/** El mínimo de monedas para pagar $cantidad, y cuáles. $dp[$v] = mínimo para pagar $v. */
function cambio(array $monedas, int $cantidad): string {
$dp = array_fill(0, $cantidad + 1, PHP_INT_MAX);
$ultima = array_fill(0, $cantidad + 1, 0);
$dp[0] = 0;
for ($v = 1; $v <= $cantidad; $v++)
foreach ($monedas as $m)
if ($m <= $v && $dp[$v - $m] !== PHP_INT_MAX && $dp[$v - $m] + 1 < $dp[$v]) {
$dp[$v] = $dp[$v - $m] + 1;
$ultima[$v] = $m; // se apunta la moneda para poder reconstruir
}
if ($dp[$cantidad] === PHP_INT_MAX) return "$cantidad: imposible";
$usadas = [];
for ($v = $cantidad; $v > 0; $v -= $ultima[$v]) $usadas[] = $ultima[$v];
return "$cantidad = " . implode(" + ", $usadas) . " ({$dp[$cantidad]} monedas)";
}
echo cambio([1, 3, 4], 6), "\n";
echo cambio([1, 5, 10, 25], 63), "\n";
echo cambio([5, 2], 3), "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
6 = 3 + 3 (2 monedas) 63 = 1 + 1 + 1 + 10 + 25 + 25 (6 monedas) 3: imposible
Traza: dp del cambio de 6 con monedas de 1, 3 y 4
| v | dp[v] | Moneda elegida | Candidatos |
|---|---|---|---|
| 0 | 0 | — | caso base |
| 1 | 1 | 1 | dp[0] + 1 = 1 |
| 2 | 2 | 1 | dp[1] + 1 = 2 |
| 3 | 1 | 3 | dp[2] + 1 = 3, dp[0] + 1 = 1 |
| 4 | 1 | 4 | dp[3] + 1 = 2, dp[1] + 1 = 2, dp[0] + 1 = 1 |
| 5 | 2 | 1 | dp[4] + 1 = 2, dp[2] + 1 = 3, dp[1] + 1 = 2 |
| 6 | 2 | 3 | dp[5] + 1 = 3, dp[3] + 1 = 2, dp[2] + 1 = 3 |
dp[6] = 2: se reconstruye desde el 6 (moneda 3) al 3 (moneda 3) al 0. El voraz habría dado 4 + 1 + 1.
Complejidad
| Problema | Fuerza bruta | Programación dinámica |
|---|---|---|
| Fibonacci, escaleras | O(2ⁿ) | O(n) |
| Cambio mínimo (k monedas, cantidad C) | Exponencial | O(C · k) |
| Mochila 0/1 (n objetos, capacidad W) | O(2ⁿ) | O(n · W) |
| Distancia de edición (textos de n y m) | Exponencial | O(n · m) |
El coste es (número de casillas) × (trabajo por casilla). La memoria es el tamaño de la tabla, aunque muchas veces basta con guardar la última fila o las últimas casillas.
- Mejor caso: O(n)
- Caso medio: O(n)
- Peor caso: O(n)
El cambio óptimo rellena cantidad + 1 casillas probando cada moneda: O(cantidad · monedas), lineal en la cantidad. Las curvas grises son las demás clases, para comparar.
En la práctica
- Los correctores ortográficos y
git diffcalculan distancias de edición y subsecuencias comunes con tablas de programación dinámica. - La bioinformática alinea secuencias de ADN y proteínas (Needleman-Wunsch, Smith-Waterman).
- El reconocimiento de voz (Viterbi), el ajuste de texto en párrafos de TeX y los planificadores de rutas usan programación dinámica.
- Los optimizadores de consultas de las bases de datos eligen el orden de los JOIN con programación dinámica.
Errores típicos
- Plantear la tabla sin definir con precisión qué significa una casilla: de ahí salen casi todos los errores de la recurrencia.
- Rellenar en un orden en que se usan casillas que aún no se han calculado.
- Olvidar los casos base o inicializarlos mal (∞ frente a 0, 1 forma de subir 0 escalones).
- Desbordar enteros al contar formas: el número de formas crece muy deprisa (usar
longoBigInteger). - Confundir «sumar las formas» (contar) con «quedarse con la mejor» (optimizar): la recurrencia cambia de
+amin/max.
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. Formas de subir una escalera
La primera línea es el número de escalones (1 a 60) y la segunda, los tamaños de paso permitidos (por ejemplo 1 2 o 1 3 5). ¿De cuántas formas distintas se puede subir la escalera? Importa el orden: subir 3 escalones como 1 + 2 es distinto que 2 + 1. Usa una tabla en la que dp[i] son las formas de subir i escalones. Completa formas.
- Entrada:
10y luego1 2. - Salida:
Formas de subir 10 escalones con pasos de 1, 2: 89(los pasos se escriben ordenados y sin repetir). - Errores:
Escalones no válidos: «…» (de 1 a 60)yPasos no válidos: «…» (enteros de 1 a 10).
Ejemplo
10 1 2
Formas de subir 10 escalones con pasos de 1, 2: 89
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Formas de subir n escalones dando pasos de los tamaños permitidos (el orden de los pasos cuenta).
5 dp[i] = suma de dp[i − p] para cada paso p ≤ i, con dp[0] = 1. */
6 static long formas(int n, int[] pasos) {
7 long[] dp = new long[n + 1];
8 dp[0] = 1;
9 for (int i = 1; i <= n; i++)
10 for (int p : pasos)
11 if (p <= i) dp[i] += dp[i - p];
12 return dp[n];
13 }
14
15 public static void main(String[] args) {
16 Scanner sc = new Scanner(System.in);
17 String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
18 String segunda = sc.hasNextLine() ? sc.nextLine().trim() : "";
19 if (!primera.matches("[1-9]\\d?") || Integer.parseInt(primera) > 60) {
20 System.out.println("Escalones no válidos: «" + primera + "» (de 1 a 60)");
21 return;
22 }
23 int n = Integer.parseInt(primera);
24 if (!segunda.matches("([1-9]|10)(\\s+([1-9]|10))*")) {
25 System.out.println("Pasos no válidos: «" + segunda + "» (enteros de 1 a 10)");
26 return;
27 }
28 int[] pasos = Arrays.stream(segunda.split("\\s+")).mapToInt(Integer::parseInt).distinct().sorted().toArray();
29 StringJoiner sj = new StringJoiner(", ");
30 for (int p : pasos) sj.add(String.valueOf(p));
31 System.out.println("Formas de subir " + n + " escalones con pasos de " + sj + ": " + formas(n, pasos));
32 }
33}Con pasos de 1 y 2 es la sucesión de Fibonacci: cada forma de subir i escalones termina en un paso de 1 (desde i − 1) o en uno de 2 (desde i − 2).
La recursividad sin memoria tardaría siglos con n = 60; la tabla hace 60 × pasos sumas.
2. El ladrón de casas
En una calle hay casas en fila con cierto dinero cada una, y la alarma salta si se entra en dos casas vecinas. ¿Cuánto se puede robar como máximo y en qué casas? Con dp[i] = lo mejor usando las casas 0..i, cada casa se roba (y entonces la anterior no) o no se roba. Para decir qué casas, se reconstruye desde el final: si dp[i] es igual a dp[i − 1], la casa i no hace falta; si no, se roba y se salta a la i − 2. Completa tabla y casas.
- Entrada: una línea con el dinero de cada casa (enteros de 0 a 9999999), por ejemplo
2 7 9 3 1. - Salida:
Máximo: 12yCasas: 1 (2), 3 (9), 5 (1)(posición desde 1 y dinero), oCasas: ninguna. - Errores:
No hay casasyValor no válido: «x».
Ejemplo
2 7 9 3 1
Máximo: 12 Casas: 1 (2), 3 (9), 5 (1)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** El ladrón de casas: el máximo que se puede robar sin entrar en dos casas vecinas.
5 dp[i] = lo mejor con las casas 0..i = max(dp[i − 1], dp[i − 2] + v[i]). */
6 static long[] tabla(int[] v) {
7 long[] dp = new long[v.length];
8 for (int i = 0; i < v.length; i++) {
9 long sin = i >= 1 ? dp[i - 1] : 0; // no robar la casa i
10 long con = (i >= 2 ? dp[i - 2] : 0) + v[i]; // robarla (y no la anterior)
11 dp[i] = Math.max(sin, con);
12 }
13 return dp;
14 }
15
16 /** Qué casas se roban (posiciones desde 0, de menor a mayor), reconstruyendo desde el final:
17 si dp[i] == dp[i − 1], la casa i no hace falta; si no, se roba y se salta a i − 2. */
18 static List<Integer> casas(int[] v, long[] dp) {
19 List<Integer> r = new ArrayList<>();
20 int i = v.length - 1;
21 while (i >= 0) {
22 if (i >= 1 && dp[i] == dp[i - 1]) i--;
23 else {
24 r.add(0, i);
25 i -= 2;
26 }
27 }
28 return r;
29 }
30
31 public static void main(String[] args) {
32 Scanner sc = new Scanner(System.in);
33 String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
34 if (linea.isEmpty()) {
35 System.out.println("No hay casas");
36 return;
37 }
38 String[] t = linea.split("\\s+");
39 int[] v = new int[t.length];
40 for (int i = 0; i < t.length; i++) {
41 if (!t[i].matches("\\d{1,7}")) {
42 System.out.println("Valor no válido: «" + t[i] + "»");
43 return;
44 }
45 v[i] = Integer.parseInt(t[i]);
46 }
47 long[] dp = tabla(v);
48 List<Integer> r = casas(v, dp);
49 StringJoiner sj = new StringJoiner(", ");
50 for (int i : r) sj.add((i + 1) + " (" + v[i] + ")");
51 System.out.println("Máximo: " + dp[v.length - 1]);
52 System.out.println("Casas: " + (r.isEmpty() ? "ninguna" : sj.toString()));
53 }
54}Cada casilla resume todas las combinaciones válidas de las casas anteriores en un solo número: por eso la tabla es lineal aunque haya 2ⁿ formas de elegir casas.
La reconstrucción repite la decisión de cada casilla al revés: si no robar la casa i daba lo mismo, no se roba (es la regla que fija qué solución se escribe cuando hay varias igual de buenas).
Test
Test: Programación dinámica
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é dos propiedades necesita un problema para resolverlo con programación dinámica?
2.¿Qué diferencia hay entre memoización y tabulación?
3.Con monedas de 1, 3 y 4, ¿cuánto vale dp[6] (mínimo de monedas para pagar 6)?
4.¿Cuál es el coste del cambio mínimo con programación dinámica para una cantidad C y k monedas?
5.Para saber QUÉ monedas usar (y no solo cuántas), ¿qué hace falta?