Problema de la mochila (0/1)
Elegir qué objetos meter en una mochila con un peso máximo para que su valor sea el mayor posible. Probarlo todo es exponencial; la programación dinámica lo resuelve con una tabla.
nivel avanzadoTambién: mochila 0/1, knapsack, knapsack problem, problema de la mochila
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.
Mochila 0/1
Escribe los pesos y los valores de los objetos y la capacidad de la mochila: la tabla se rellena casilla a casilla y al final se reconstruye qué objetos entran.
Paso 1
Una fila por objeto y una columna por capacidad, de 0 a 7. La casilla dp[i][w] guarda el mayor valor que se consigue con los i primeros objetos en una mochila de w kg. Sin objetos (fila de arriba), todo vale 0.
1static int mochila(int[] peso, int[] valor, int capacidad, List<Integer> elegidos) {
2 int n = peso.length;
3 int[][] dp = new int[n + 1][capacidad + 1]; // n = 4, capacidad = 7
4 for (int i = 1; i <= n; i++)
5 for (int w = 0; w <= capacidad; w++) {
6 dp[i][w] = dp[i - 1][w];
7 if (peso[i - 1] <= w)
8 dp[i][w] = Math.max(dp[i][w], dp[i - 1][w - peso[i - 1]] + valor[i - 1]);
9 }
10 for (int i = n, w = capacidad; i > 0; i--)
11 if (dp[i][w] != dp[i - 1][w]) { // si cambió, el objeto i entró
12 elegidos.add(i - 1);
13 w -= peso[i - 1];
14 }
15 return dp[n][capacidad];
16}Variables
- n
- 4
- capacidad
- 7
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Hay n objetos, cada uno con su peso y su valor, y una mochila que aguanta W kilos. Cada objeto entra entero o no entra (por eso es la mochila 0/1). ¿Qué combinación da más valor sin pasarse del peso? Coger primero lo más valioso, o lo que más vale por kilo, parece razonable y falla: con 10 kg y objetos de 6 kg (30 €), 3 kg (14 €), 4 kg (16 €) y 2 kg (9 €), el voraz se queda en 44 € y lo mejor son 46.
Probar todas las combinaciones funciona, pero con n objetos hay 2ⁿ subconjuntos: con 30 objetos, más de mil millones. La salida es la programación dinámica. Se define dp[i][w] como el mejor valor que se consigue con los i primeros objetos en una mochila de w kilos, y cada casilla sale de dos de la fila de arriba: sin el objeto i, dp[i − 1][w]; con él, si cabe, su valor más dp[i − 1][w − peso], lo mejor que se puede hacer con el sitio que deja libre.
La tabla se rellena fila a fila (un objeto más cada vez) y la respuesta queda en la esquina, dp[n][W]. Para saber qué objetos son, se recorre al revés: si una casilla es distinta de la de arriba, ese objeto entró y se baja su peso de la columna; si es igual, no hacía falta.
El coste es O(n · W): pseudopolinómico, porque depende del valor de W y no de cuántos datos hay. Con capacidades de millones la tabla se vuelve enorme. Una mejora habitual es guardar una sola fila y recorrer w de mayor a menor (al revés, cada objeto se podría usar varias veces: eso es la mochila ilimitada). Y si los objetos se pueden partir (la mochila fraccionaria), basta el voraz por valor por kilo.
Cuándo usarlo
- Elegir un subconjunto con un límite: proyectos con presupuesto, tareas en un tiempo dado, anuncios en un espacio.
- Cargar un camión, un contenedor o una mochila cuando importa una sola medida (peso o volumen).
- Variantes con la misma tabla: suma de subconjuntos (¿se puede formar justo esta cantidad?), repartir en dos mitades iguales, cambio de monedas.
- Cualquier problema en el que cada elemento se coge o no y hay que optimizar una suma con una restricción.
Cuándo no
- Si los objetos se pueden partir (arena, líquidos, tiempo): la mochila fraccionaria se resuelve con el voraz por €/kg.
- Si la capacidad es enorme (millones) y hay pocos objetos: la tabla no cabe; mejor backtracking con poda o partir los objetos en dos mitades.
- Si hay varias restricciones a la vez (peso y volumen): la tabla gana una dimensión por cada una y crece muy deprisa.
Paso a paso
- Definir el subproblema. dp[i][w] = el mejor valor con los i primeros objetos y w kilos de capacidad.
- Caso base. Sin objetos (fila 0) el valor es 0 para cualquier capacidad.
- Recurrencia. dp[i][w] = el máximo entre no coger el objeto (dp[i − 1][w]) y cogerlo si cabe (valor + dp[i − 1][w − peso]).
- Rellenar y reconstruir. Fila a fila hasta dp[n][W]. Después, desde la esquina hacia arriba: donde una casilla cambia respecto a la de arriba, ese objeto entró.
El código
La tabla y los objetos elegidos
Los cuatro objetos del visualizador: se imprime la tabla entera y se reconstruye qué entra.
1import java.util.*;
2
3public class Main {
4 public static void main(String[] args) {
5 String[] nombre = {"agua", "libro", "linterna", "tablet"};
6 int[] peso = {1, 3, 4, 5};
7 int[] valor = {1, 4, 5, 7};
8 int capacidad = 7, n = peso.length;
9
10 int[][] dp = new int[n + 1][capacidad + 1]; // dp[i][w]: lo mejor con los i primeros y w kg
11 for (int i = 1; i <= n; i++)
12 for (int w = 0; w <= capacidad; w++) {
13 dp[i][w] = dp[i - 1][w]; // sin el objeto i
14 if (peso[i - 1] <= w) // con él, si cabe
15 dp[i][w] = Math.max(dp[i][w], dp[i - 1][w - peso[i - 1]] + valor[i - 1]);
16 }
17
18 StringBuilder cabecera = new StringBuilder(String.format("%-10s", "kg:"));
19 for (int w = 0; w <= capacidad; w++) cabecera.append(String.format("%3d", w));
20 System.out.println(cabecera);
21 for (int i = 0; i <= n; i++) {
22 StringBuilder fila = new StringBuilder(String.format("%-10s", i == 0 ? "-" : nombre[i - 1]));
23 for (int w = 0; w <= capacidad; w++) fila.append(String.format("%3d", dp[i][w]));
24 System.out.println(fila);
25 }
26
27 List<String> dentro = new ArrayList<>();
28 int w = capacidad;
29 for (int i = n; i > 0; i--)
30 if (dp[i][w] != dp[i - 1][w]) { // distinto de la fila de arriba: entró
31 dentro.add(0, nombre[i - 1]);
32 w -= peso[i - 1];
33 }
34 System.out.println("Valor máximo: " + dp[n][capacidad] + " con " + String.join(" y ", dentro) + " (" + (capacidad - w) + " de " + capacidad + " kg)");
35 }
36}nombre = ["agua", "libro", "linterna", "tablet"]
peso = [1, 3, 4, 5]
valor = [1, 4, 5, 7]
capacidad, n = 7, len(peso)
dp = [[0] * (capacidad + 1) for _ in range(n + 1)] # dp[i][w]: lo mejor con los i primeros y w kg
for i in range(1, n + 1):
for w in range(capacidad + 1):
dp[i][w] = dp[i - 1][w] # sin el objeto i
if peso[i - 1] <= w: # con él, si cabe
dp[i][w] = max(dp[i][w], dp[i - 1][w - peso[i - 1]] + valor[i - 1])
print("kg:".ljust(10) + "".join(f"{w:3d}" for w in range(capacidad + 1)))
for i in range(n + 1):
etiqueta = "-" if i == 0 else nombre[i - 1]
print(etiqueta.ljust(10) + "".join(f"{dp[i][w]:3d}" for w in range(capacidad + 1)))
dentro = []
w = capacidad
for i in range(n, 0, -1):
if dp[i][w] != dp[i - 1][w]: # distinto de la fila de arriba: entró
dentro.insert(0, nombre[i - 1])
w -= peso[i - 1]
print(f"Valor máximo: {dp[n][capacidad]} con {' y '.join(dentro)} ({capacidad - w} de {capacidad} kg)")const nombre = ["agua", "libro", "linterna", "tablet"];
const peso = [1, 3, 4, 5];
const valor = [1, 4, 5, 7];
const capacidad = 7, n = peso.length;
const dp = Array.from({ length: n + 1 }, () => new Array(capacidad + 1).fill(0)); // dp[i][w]: lo mejor con los i primeros y w kg
for (let i = 1; i <= n; i++)
for (let w = 0; w <= capacidad; w++) {
dp[i][w] = dp[i - 1][w]; // sin el objeto i
if (peso[i - 1] <= w) // con él, si cabe
dp[i][w] = Math.max(dp[i][w], dp[i - 1][w - peso[i - 1]] + valor[i - 1]);
}
let cabecera = "kg:".padEnd(10);
for (let w = 0; w <= capacidad; w++) cabecera += String(w).padStart(3);
console.log(cabecera);
for (let i = 0; i <= n; i++) {
let fila = (i === 0 ? "-" : nombre[i - 1]).padEnd(10);
for (let w = 0; w <= capacidad; w++) fila += String(dp[i][w]).padStart(3);
console.log(fila);
}
const dentro = [];
let w = capacidad;
for (let i = n; i > 0; i--)
if (dp[i][w] !== dp[i - 1][w]) { // distinto de la fila de arriba: entró
dentro.unshift(nombre[i - 1]);
w -= peso[i - 1];
}
console.log(`Valor máximo: ${dp[n][capacidad]} con ${dentro.join(" y ")} (${capacidad - w} de ${capacidad} kg)`);using System;
using System.Collections.Generic;
class Program {
static void Main() {
string[] nombre = { "agua", "libro", "linterna", "tablet" };
int[] peso = { 1, 3, 4, 5 };
int[] valor = { 1, 4, 5, 7 };
int capacidad = 7, n = peso.Length;
int[,] dp = new int[n + 1, capacidad + 1]; // dp[i, w]: lo mejor con los i primeros y w kg
for (int i = 1; i <= n; i++)
for (int w = 0; w <= capacidad; w++) {
dp[i, w] = dp[i - 1, w]; // sin el objeto i
if (peso[i - 1] <= w) // con él, si cabe
dp[i, w] = Math.Max(dp[i, w], dp[i - 1, w - peso[i - 1]] + valor[i - 1]);
}
string cabecera = "kg:".PadRight(10);
for (int w = 0; w <= capacidad; w++) cabecera += w.ToString().PadLeft(3);
Console.WriteLine(cabecera);
for (int i = 0; i <= n; i++) {
string fila = (i == 0 ? "-" : nombre[i - 1]).PadRight(10);
for (int w = 0; w <= capacidad; w++) fila += dp[i, w].ToString().PadLeft(3);
Console.WriteLine(fila);
}
var dentro = new List<string>();
int kg = capacidad;
for (int i = n; i > 0; i--)
if (dp[i, kg] != dp[i - 1, kg]) { // distinto de la fila de arriba: entró
dentro.Insert(0, nombre[i - 1]);
kg -= peso[i - 1];
}
Console.WriteLine(quot;Valor máximo: {dp[n, capacidad]} con {string.Join(" y ", dentro)} ({capacidad - kg} de {capacidad} kg)");
}
}<?php
$nombre = ["agua", "libro", "linterna", "tablet"];
$peso = [1, 3, 4, 5];
$valor = [1, 4, 5, 7];
$capacidad = 7;
$n = count($peso);
$dp = array_fill(0, $n + 1, array_fill(0, $capacidad + 1, 0)); // $dp[i][w]: lo mejor con los i primeros y w kg
for ($i = 1; $i <= $n; $i++)
for ($w = 0; $w <= $capacidad; $w++) {
$dp[$i][$w] = $dp[$i - 1][$w]; // sin el objeto i
if ($peso[$i - 1] <= $w) // con él, si cabe
$dp[$i][$w] = max($dp[$i][$w], $dp[$i - 1][$w - $peso[$i - 1]] + $valor[$i - 1]);
}
$cabecera = sprintf("%-10s", "kg:");
for ($w = 0; $w <= $capacidad; $w++) $cabecera .= sprintf("%3d", $w);
echo $cabecera . "\n";
for ($i = 0; $i <= $n; $i++) {
$fila = sprintf("%-10s", $i == 0 ? "-" : $nombre[$i - 1]);
for ($w = 0; $w <= $capacidad; $w++) $fila .= sprintf("%3d", $dp[$i][$w]);
echo $fila . "\n";
}
$dentro = [];
$w = $capacidad;
for ($i = $n; $i > 0; $i--)
if ($dp[$i][$w] != $dp[$i - 1][$w]) { // distinto de la fila de arriba: entró
array_unshift($dentro, $nombre[$i - 1]);
$w -= $peso[$i - 1];
}
echo "Valor máximo: {$dp[$n][$capacidad]} con " . implode(" y ", $dentro) . " (" . ($capacidad - $w) . " de $capacidad kg)\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
kg: 0 1 2 3 4 5 6 7 - 0 0 0 0 0 0 0 0 agua 0 1 1 1 1 1 1 1 libro 0 1 1 4 5 5 5 5 linterna 0 1 1 4 5 6 6 9 tablet 0 1 1 4 5 7 8 9 Valor máximo: 9 con libro y linterna (7 de 7 kg)
Voraz, fuerza bruta y tabla
El mismo problema resuelto de tres formas: el voraz por €/kg se equivoca, la fuerza bruta prueba los 16 subconjuntos y la tabla de una sola fila acierta con 11 casillas.
1import java.util.*;
2
3public class Main {
4 static final int[] PESO = {6, 3, 4, 2};
5 static final int[] VALOR = {30, 14, 16, 9};
6 static final int CAPACIDAD = 10;
7
8 /** Voraz: por orden de €/kg, cada objeto que quepa. Rápido, pero en la mochila 0/1 puede fallar. */
9 static int voraz() {
10 Integer[] orden = {0, 1, 2, 3};
11 Arrays.sort(orden, (a, b) -> VALOR[b] * PESO[a] - VALOR[a] * PESO[b]); // más €/kg primero, sin decimales
12 int libre = CAPACIDAD, total = 0;
13 for (int i : orden)
14 if (PESO[i] <= libre) {
15 libre -= PESO[i];
16 total += VALOR[i];
17 }
18 return total;
19 }
20
21 /** Fuerza bruta: cada número de 0 a 2^n − 1 es un subconjunto (el bit k dice si el objeto k entra). */
22 static int fuerzaBruta() {
23 int mejor = 0, n = PESO.length;
24 for (int mascara = 0; mascara < (1 << n); mascara++) {
25 int p = 0, v = 0;
26 for (int k = 0; k < n; k++)
27 if ((mascara & (1 << k)) != 0) {
28 p += PESO[k];
29 v += VALOR[k];
30 }
31 if (p <= CAPACIDAD) mejor = Math.max(mejor, v);
32 }
33 return mejor;
34 }
35
36 /** La tabla con una sola fila: w de mayor a menor, para no usar un objeto dos veces. */
37 static int tabla() {
38 int[] dp = new int[CAPACIDAD + 1];
39 for (int i = 0; i < PESO.length; i++)
40 for (int w = CAPACIDAD; w >= PESO[i]; w--)
41 dp[w] = Math.max(dp[w], dp[w - PESO[i]] + VALOR[i]);
42 return dp[CAPACIDAD];
43 }
44
45 public static void main(String[] args) {
46 System.out.println("Voraz por €/kg: " + voraz());
47 System.out.println("Fuerza bruta, " + (1 << PESO.length) + " subconjuntos: " + fuerzaBruta());
48 System.out.println("Tabla de una fila, " + (CAPACIDAD + 1) + " casillas: " + tabla());
49 System.out.println("El voraz coge 6 kg + 3 kg (44) y ya no le cabe nada; lo mejor es 6 kg + 4 kg (46).");
50 }
51}from functools import cmp_to_key
PESO = [6, 3, 4, 2]
VALOR = [30, 14, 16, 9]
CAPACIDAD = 10
def voraz():
"""Voraz: por orden de €/kg, cada objeto que quepa. Rápido, pero en la mochila 0/1 puede fallar."""
orden = sorted(range(4), key=cmp_to_key(lambda a, b: VALOR[b] * PESO[a] - VALOR[a] * PESO[b])) # más €/kg primero, sin decimales
libre, total = CAPACIDAD, 0
for i in orden:
if PESO[i] <= libre:
libre -= PESO[i]
total += VALOR[i]
return total
def fuerza_bruta():
"""Fuerza bruta: cada número de 0 a 2^n − 1 es un subconjunto (el bit k dice si el objeto k entra)."""
mejor, n = 0, len(PESO)
for mascara in range(1 << n):
p = v = 0
for k in range(n):
if mascara & (1 << k):
p += PESO[k]
v += VALOR[k]
if p <= CAPACIDAD:
mejor = max(mejor, v)
return mejor
def tabla():
"""La tabla con una sola fila: w de mayor a menor, para no usar un objeto dos veces."""
dp = [0] * (CAPACIDAD + 1)
for i in range(len(PESO)):
for w in range(CAPACIDAD, PESO[i] - 1, -1):
dp[w] = max(dp[w], dp[w - PESO[i]] + VALOR[i])
return dp[CAPACIDAD]
print(f"Voraz por €/kg: {voraz()}")
print(f"Fuerza bruta, {1 << len(PESO)} subconjuntos: {fuerza_bruta()}")
print(f"Tabla de una fila, {CAPACIDAD + 1} casillas: {tabla()}")
print("El voraz coge 6 kg + 3 kg (44) y ya no le cabe nada; lo mejor es 6 kg + 4 kg (46).")const PESO = [6, 3, 4, 2];
const VALOR = [30, 14, 16, 9];
const CAPACIDAD = 10;
/** Voraz: por orden de €/kg, cada objeto que quepa. Rápido, pero en la mochila 0/1 puede fallar. */
function voraz() {
const orden = [0, 1, 2, 3].sort((a, b) => VALOR[b] * PESO[a] - VALOR[a] * PESO[b]); // más €/kg primero, sin decimales
let libre = CAPACIDAD, total = 0;
for (const i of orden)
if (PESO[i] <= libre) {
libre -= PESO[i];
total += VALOR[i];
}
return total;
}
/** Fuerza bruta: cada número de 0 a 2^n − 1 es un subconjunto (el bit k dice si el objeto k entra). */
function fuerzaBruta() {
let mejor = 0;
const n = PESO.length;
for (let mascara = 0; mascara < (1 << n); mascara++) {
let p = 0, v = 0;
for (let k = 0; k < n; k++)
if (mascara & (1 << k)) {
p += PESO[k];
v += VALOR[k];
}
if (p <= CAPACIDAD) mejor = Math.max(mejor, v);
}
return mejor;
}
/** La tabla con una sola fila: w de mayor a menor, para no usar un objeto dos veces. */
function tabla() {
const dp = new Array(CAPACIDAD + 1).fill(0);
for (let i = 0; i < PESO.length; i++)
for (let w = CAPACIDAD; w >= PESO[i]; w--)
dp[w] = Math.max(dp[w], dp[w - PESO[i]] + VALOR[i]);
return dp[CAPACIDAD];
}
console.log(`Voraz por €/kg: ${voraz()}`);
console.log(`Fuerza bruta, ${1 << PESO.length} subconjuntos: ${fuerzaBruta()}`);
console.log(`Tabla de una fila, ${CAPACIDAD + 1} casillas: ${tabla()}`);
console.log("El voraz coge 6 kg + 3 kg (44) y ya no le cabe nada; lo mejor es 6 kg + 4 kg (46).");using System;
using System.Collections.Generic;
using System.Linq;
class Program {
static readonly int[] PESO = { 6, 3, 4, 2 };
static readonly int[] VALOR = { 30, 14, 16, 9 };
const int CAPACIDAD = 10;
/// Voraz: por orden de €/kg, cada objeto que quepa. Rápido, pero en la mochila 0/1 puede fallar.
static int Voraz() {
var orden = new[] { 0, 1, 2, 3 }.OrderBy(i => i, Comparer<int>.Create((a, b) => VALOR[b] * PESO[a] - VALOR[a] * PESO[b])); // más €/kg primero
int libre = CAPACIDAD, total = 0;
foreach (int i in orden)
if (PESO[i] <= libre) {
libre -= PESO[i];
total += VALOR[i];
}
return total;
}
/// Fuerza bruta: cada número de 0 a 2^n − 1 es un subconjunto (el bit k dice si el objeto k entra).
static int FuerzaBruta() {
int mejor = 0, n = PESO.Length;
for (int mascara = 0; mascara < (1 << n); mascara++) {
int p = 0, v = 0;
for (int k = 0; k < n; k++)
if ((mascara & (1 << k)) != 0) {
p += PESO[k];
v += VALOR[k];
}
if (p <= CAPACIDAD) mejor = Math.Max(mejor, v);
}
return mejor;
}
/// La tabla con una sola fila: w de mayor a menor, para no usar un objeto dos veces.
static int Tabla() {
int[] dp = new int[CAPACIDAD + 1];
for (int i = 0; i < PESO.Length; i++)
for (int w = CAPACIDAD; w >= PESO[i]; w--)
dp[w] = Math.Max(dp[w], dp[w - PESO[i]] + VALOR[i]);
return dp[CAPACIDAD];
}
static void Main() {
Console.WriteLine("Voraz por €/kg: " + Voraz());
Console.WriteLine("Fuerza bruta, " + (1 << PESO.Length) + " subconjuntos: " + FuerzaBruta());
Console.WriteLine("Tabla de una fila, " + (CAPACIDAD + 1) + " casillas: " + Tabla());
Console.WriteLine("El voraz coge 6 kg + 3 kg (44) y ya no le cabe nada; lo mejor es 6 kg + 4 kg (46).");
}
}<?php
const PESO = [6, 3, 4, 2];
const VALOR = [30, 14, 16, 9];
const CAPACIDAD = 10;
/** Voraz: por orden de €/kg, cada objeto que quepa. Rápido, pero en la mochila 0/1 puede fallar. */
function voraz(): int {
$orden = [0, 1, 2, 3];
usort($orden, fn($a, $b) => VALOR[$b] * PESO[$a] - VALOR[$a] * PESO[$b]); // más €/kg primero, sin decimales
$libre = CAPACIDAD;
$total = 0;
foreach ($orden as $i)
if (PESO[$i] <= $libre) {
$libre -= PESO[$i];
$total += VALOR[$i];
}
return $total;
}
/** Fuerza bruta: cada número de 0 a 2^n − 1 es un subconjunto (el bit k dice si el objeto k entra). */
function fuerzaBruta(): int {
$mejor = 0;
$n = count(PESO);
for ($mascara = 0; $mascara < (1 << $n); $mascara++) {
$p = 0;
$v = 0;
for ($k = 0; $k < $n; $k++)
if ($mascara & (1 << $k)) {
$p += PESO[$k];
$v += VALOR[$k];
}
if ($p <= CAPACIDAD) $mejor = max($mejor, $v);
}
return $mejor;
}
/** La tabla con una sola fila: w de mayor a menor, para no usar un objeto dos veces. */
function tabla(): int {
$dp = array_fill(0, CAPACIDAD + 1, 0);
for ($i = 0; $i < count(PESO); $i++)
for ($w = CAPACIDAD; $w >= PESO[$i]; $w--)
$dp[$w] = max($dp[$w], $dp[$w - PESO[$i]] + VALOR[$i]);
return $dp[CAPACIDAD];
}
echo "Voraz por €/kg: " . voraz() . "\n";
echo "Fuerza bruta, " . (1 << count(PESO)) . " subconjuntos: " . fuerzaBruta() . "\n";
echo "Tabla de una fila, " . (CAPACIDAD + 1) . " casillas: " . tabla() . "\n";
echo "El voraz coge 6 kg + 3 kg (44) y ya no le cabe nada; lo mejor es 6 kg + 4 kg (46).\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Voraz por €/kg: 44 Fuerza bruta, 16 subconjuntos: 46 Tabla de una fila, 11 casillas: 46 El voraz coge 6 kg + 3 kg (44) y ya no le cabe nada; lo mejor es 6 kg + 4 kg (46).
Traza: La tabla del visualizador (capacidad 7 kg)
| Objetos | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| (ninguno) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| agua (1 kg, 1 €) | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| libro (3 kg, 4 €) | 0 | 1 | 1 | 4 | 5 | 5 | 5 | 5 |
| linterna (4 kg, 5 €) | 0 | 1 | 1 | 4 | 5 | 6 | 6 | 9 |
| tablet (5 kg, 7 €) | 0 | 1 | 1 | 4 | 5 | 7 | 8 | 9 |
Cada fila añade un objeto. La esquina (9) es la respuesta: libro + linterna. La tablet sola (7) o con el agua (8) no llega.
Complejidad
| Método | Tiempo | Memoria |
|---|---|---|
| Fuerza bruta (todos los subconjuntos) | O(2ⁿ · n) | O(n) |
| Tabla (programación dinámica) | O(n · W) | O(n · W) |
| Tabla de una sola fila | O(n · W) | O(W) |
| Voraz por €/kg (solo vale en la fraccionaria) | O(n log n) | O(1) |
O(n · W) es pseudopolinómico: crece con el valor de W, no con los datos. La mochila 0/1 es NP-difícil: no se conoce ningún algoritmo polinómico en el tamaño de la entrada.
- Mejor caso: O(n²)
- Caso medio: O(n²)
- Peor caso: O(n²)
En realidad O(n · W): una casilla por objeto y kilo de capacidad. Probar todos los subconjuntos sería O(2ⁿ); ojo, si W es enorme la tabla también lo es. Las curvas grises son las demás clases, para comparar.
En la práctica
- Selección de proyectos o inversiones con un presupuesto limitado.
- Carga de vehículos y contenedores (y su primo, el empaquetado en cajas, *bin packing*).
- Planificar qué tareas caben en un sprint o qué anuncios entran en un bloque publicitario.
- Asignación de recursos en la nube: qué procesos caben en una máquina con memoria limitada.
- Fue la base de uno de los primeros sistemas de cifrado de clave pública (Merkle-Hellman), que acabó roto.
Errores típicos
- Resolver la 0/1 con el voraz por valor o por €/kg: da buenas soluciones, pero no siempre la mejor.
- Con una sola fila, recorrer w de menor a mayor: un objeto se puede usar varias veces y se resuelve otro problema (la mochila ilimitada).
- Olvidar la columna 0 o la fila 0: la tabla es de (n + 1) × (W + 1).
- Confundir índices: el objeto de la fila i es el i − 1 del array.
- Leer la respuesta en dp[n][W] y no reconstruir: el enunciado suele pedir también qué objetos son.
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. Presupuesto de proyectos
Una empresa tiene un presupuesto y varios proyectos posibles, cada uno con su coste y su beneficio. Cada proyecto se hace entero o no se hace. Elige los que dan más beneficio sin pasarse del presupuesto. El main lee los datos y escribe el resultado: completa elegir con la tabla de la mochila.
- Primera línea: el presupuesto (entero de 0 a 9999). Después, un proyecto por línea:
nombre coste beneficio(coste de 1 a 9999). - Salida:
Proyectos: web, app(en el orden de la entrada) yBeneficio: 120 · Coste: 95 de 100, oNo cabe ningún proyecto. - Líneas mal escritas:
Línea no válida: «…». En las pruebas, la mejor combinación es única.
Ejemplo
100 web 40 50 app 55 70 tienda 60 75 blog 10 12
Proyectos: web, tienda Beneficio: 125 · Coste: 100 de 100
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Las posiciones (de menor a mayor) de los proyectos que dan más beneficio sin pasarse del presupuesto. */
5 static List<Integer> elegir(int[] coste, int[] beneficio, int presupuesto) {
6 int n = coste.length;
7 int[][] dp = new int[n + 1][presupuesto + 1];
8 for (int i = 1; i <= n; i++)
9 for (int w = 0; w <= presupuesto; w++) {
10 dp[i][w] = dp[i - 1][w];
11 if (coste[i - 1] <= w) dp[i][w] = Math.max(dp[i][w], dp[i - 1][w - coste[i - 1]] + beneficio[i - 1]);
12 }
13 LinkedList<Integer> elegidos = new LinkedList<>();
14 for (int i = n, w = presupuesto; i > 0; i--)
15 if (dp[i][w] != dp[i - 1][w]) { // distinto de la fila de arriba: entró
16 elegidos.addFirst(i - 1);
17 w -= coste[i - 1];
18 }
19 return elegidos;
20 }
21
22 public static void main(String[] args) {
23 Scanner sc = new Scanner(System.in);
24 String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
25 if (!primera.matches("\\d{1,4}")) {
26 System.out.println("Presupuesto no válido: «" + primera + "»");
27 return;
28 }
29 int presupuesto = Integer.parseInt(primera);
30 List<String> nombres = new ArrayList<>();
31 List<Integer> costes = new ArrayList<>(), beneficios = new ArrayList<>();
32 while (sc.hasNextLine()) {
33 String linea = sc.nextLine().trim();
34 if (linea.isEmpty()) continue;
35 String[] p = linea.split("\\s+");
36 if (p.length != 3 || !p[1].matches("[1-9]\\d{0,3}") || !p[2].matches("\\d{1,5}")) {
37 System.out.println("Línea no válida: «" + linea + "»");
38 continue;
39 }
40 nombres.add(p[0]);
41 costes.add(Integer.parseInt(p[1]));
42 beneficios.add(Integer.parseInt(p[2]));
43 }
44 int[] c = costes.stream().mapToInt(Integer::intValue).toArray();
45 int[] b = beneficios.stream().mapToInt(Integer::intValue).toArray();
46 int coste = 0, beneficio = 0;
47 List<String> lista = new ArrayList<>();
48 for (int i : elegir(c, b, presupuesto)) {
49 coste += c[i];
50 beneficio += b[i];
51 lista.add(nombres.get(i));
52 }
53 if (lista.isEmpty()) System.out.println("No cabe ningún proyecto");
54 else {
55 System.out.println("Proyectos: " + String.join(", ", lista));
56 System.out.println("Beneficio: " + beneficio + " · Coste: " + coste + " de " + presupuesto);
57 }
58 }
59}Cada casilla resuelve un problema más pequeño (menos proyectos, menos presupuesto) y se apoya en dos de la fila anterior: con 30 proyectos y 1000 € son 31.000 casillas, frente a más de mil millones de combinaciones.
La reconstrucción no necesita guardar nada más: basta comparar cada casilla con la de arriba.
2. Repartir la carga en dos camiones
Hay que repartir unos paquetes entre dos camiones para que vayan lo más igualados posible. Escribe cuánto lleva cada uno y la diferencia. El truco: si un camión lleva s kg, el otro lleva total − s, así que basta encontrar la suma más cercana a la mitad que se pueda formar con algunos paquetes. El main lee los pesos: completa mejorMitad.
- Entrada: los pesos de los paquetes (enteros de 1 a 1000), separados por espacios o saltos de línea. Un valor que no lo sea:
Peso no válido: «…»(y se ignora). - Salida:
7 paquetes, 47 kg en totalyCami ón 1: 23 kg · Camión 2: 24 kg · Diferencia: 1 kg(el camión 1, el que menos lleva). - Sin paquetes válidos:
No hay paquetes.
Ejemplo
8 7 6 5 4 9 8
7 paquetes, 47 kg en total Camión 1: 23 kg · Camión 2: 24 kg · Diferencia: 1 kg
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** El mayor peso, sin pasar de la mitad del total, que se puede formar con algunos de los paquetes. */
5 static int mejorMitad(int[] pesos) {
6 int total = 0;
7 for (int p : pesos) total += p;
8 boolean[] posible = new boolean[total / 2 + 1]; // posible[s]: ¿hay paquetes que sumen justo s?
9 posible[0] = true;
10 for (int p : pesos)
11 for (int s = total / 2; s >= p; s--) // de mayor a menor: cada paquete, una sola vez
12 if (posible[s - p]) posible[s] = true;
13 int s = total / 2;
14 while (!posible[s]) s--;
15 return s;
16 }
17
18 public static void main(String[] args) {
19 Scanner sc = new Scanner(System.in);
20 List<Integer> pesos = new ArrayList<>();
21 while (sc.hasNext()) {
22 String t = sc.next();
23 if (t.matches("[1-9]\\d{0,3}") && Integer.parseInt(t) <= 1000) pesos.add(Integer.parseInt(t));
24 else System.out.println("Peso no válido: «" + t + "»");
25 }
26 if (pesos.isEmpty()) {
27 System.out.println("No hay paquetes");
28 return;
29 }
30 int[] p = pesos.stream().mapToInt(Integer::intValue).toArray();
31 int total = Arrays.stream(p).sum();
32 int uno = mejorMitad(p), otro = total - uno;
33 System.out.println(p.length + (p.length == 1 ? " paquete, " : " paquetes, ") + total + " kg en total");
34 System.out.println("Camión 1: " + uno + " kg · Camión 2: " + otro + " kg · Diferencia: " + (otro - uno) + " kg");
35 }
36}Repartir «a ojo» (el más pesado al camión que menos lleve) suele quedarse cerca, pero no garantiza el mejor reparto. La tabla de booleanos prueba todas las sumas posibles sin probar todas las combinaciones: O(n · total).
Es el problema de la partición, un caso particular de la suma de subconjuntos y, por tanto, de la mochila.
Test
Test: Problema de la mochila (0/1)
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é guarda dp[i][w] en la mochila 0/1?
2.¿Por qué no basta coger los objetos de mayor €/kg en la mochila 0/1?
3.¿Qué coste tiene la tabla de la mochila con n objetos y capacidad W?
4.Con una sola fila, ¿por qué se recorre w de mayor a menor?
5.¿Cómo se sabe qué objetos entran, mirando la tabla terminada?