Problema de la mochila (Knapsack)
Conocimientos previos: Introducción a la Programación Dinámica
Introducción
Consideremos el siguiente ejemplo:
[USACO07 Dec] Charm Bracelet
Hay ítems distintos y una mochila (knapsack) de capacidad . Cada ítem tiene 2 atributos: peso () y valor (). Hay que seleccionar un subconjunto de ítems para poner en la mochila de modo que el peso total no exceda la capacidad y el valor total se maximice.
En el ejemplo anterior, cada objeto tiene solo dos estados posibles (tomado o no tomado), que corresponden al binario 0 y 1. Por eso este tipo de problema se llama “problema de la mochila 0-1” (0-1 knapsack).
Mochila 0-1 (0-1 Knapsack)
Explicación
En el ejemplo anterior, la entrada del problema es la siguiente: el peso del ítem , el valor del ítem , y la capacidad total de la mochila .
Sea el estado de programación dinámica que guarda el valor total máximo que la mochila puede llevar con capacidad , cuando solo se consideran los primeros ítems.
Suponiendo que ya se procesaron todos los estados de los primeros ítems, ¿cuáles son las opciones para el ítem?
- Cuando no se pone en la mochila, la capacidad restante no cambia y el valor total tampoco. Por lo tanto, el valor máximo en este caso es
- Cuando se pone en la mochila, la capacidad restante disminuye en y el valor total aumenta en , así que el valor máximo en este caso es
De aquí podemos derivar la ecuación de transición de la DP:
Además, como solo depende de , podemos eliminar la primera dimensión. Obtenemos la regla de transición
que debe ejecutarse en orden decreciente de (de modo que corresponda implícitamente a y no a ).
Es importante entender esta regla de transición, porque la mayoría de las transiciones de los problemas de mochila se derivan de forma similar.
Implementación
El algoritmo descrito se puede implementar en así:
for (int i = 1; i <= n; i++)
for (int j = W; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + v[i]);De nuevo, nótese el orden de ejecución. Debe seguirse estrictamente para garantizar el siguiente invariante: justo antes de procesar el par , corresponde a para , pero a para . Esto asegura que se toma del paso -ésimo, y no del -ésimo.
Mochila completa (Complete Knapsack)
El modelo de mochila completa es similar a la mochila 0-1; la única diferencia es que un ítem se puede seleccionar un número ilimitado de veces en lugar de solo una.
Podemos retomar la idea de la mochila 0-1 para definir el estado: , el valor máximo que la mochila puede obtener usando los primeros ítems con capacidad máxima .
Cabe señalar que, aunque la definición del estado es similar a la de la mochila 0-1, su regla de transición es distinta.
Explicación
El enfoque trivial es, para los primeros ítems, enumerar cuántas veces se toma cada ítem. La complejidad temporal de esto es .
Esto produce la siguiente ecuación de transición:
Al mismo tiempo, se simplifica en una ecuación “plana”:
La razón por la que esto funciona es que ya fue actualizado por y así sucesivamente.
Al igual que en la mochila 0-1, podemos eliminar la primera dimensión para optimizar la complejidad espacial. Esto nos da la misma regla de transición que la mochila 0-1.
Implementación
El algoritmo descrito se puede implementar en así:
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= W; j++)
f[j] = max(f[j], f[j - w[i]] + v[i]);A pesar de tener la misma regla de transición, el código anterior es incorrecto para la mochila 0-1.
Observando el código con cuidado, vemos que para el ítem que se está procesando y el estado actual , cuando , se verá afectado por . Esto equivale a poder poner el ítem en la mochila varias veces, lo cual es coherente con el problema de la mochila completa y no con el de la mochila 0-1.
Mochila múltiple (Multiple Knapsack)
La mochila múltiple también es una variante de la mochila 0-1. La diferencia principal es que hay unidades de cada ítem en lugar de solo .
Explicación
Una idea muy simple es: “elegir cada ítem veces” equivale a “se seleccionan copias del mismo ítem una por una”. Así se convierte en un modelo de mochila 0-1, que se puede describir con la función de transición:
La complejidad temporal de este proceso es
Optimización por agrupamiento binario
Seguimos considerando convertir el modelo de mochila múltiple en un modelo de mochila 0-1 para optimizarlo. La complejidad temporal no se puede optimizar más con el enfoque anterior, así que nos concentramos en el componente .
Sea el ítem obtenido al partir el ítem. En el enfoque trivial discutido arriba, representa el mismo ítem para todo . La razón principal de nuestra baja eficiencia es que hacemos mucho trabajo repetitivo. Por ejemplo, considerar seleccionar y seleccionar . Estas dos situaciones son completamente equivalentes. Por lo tanto, optimizar el método de partición reducirá mucho la complejidad temporal.
El agrupamiento se vuelve más eficiente usando agrupamiento binario.
En concreto, contiene ítems individuales (). Si no es una potencia entera de , se usa otro paquete de tamaño para completar.
Con el método de partición anterior, es posible obtener cualquier suma de ítems seleccionando algunos . Tras partir cada ítem de la forma descrita, basta usar el método de mochila 0-1 para resolver la nueva formulación del problema.
Esta optimización nos da una complejidad temporal de .
Implementación
index = 0;
for (int i = 1; i <= n; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k > c) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}Optimización con cola monótona
En esta optimización, el objetivo es convertir el problema de la mochila en uno de cola de máximos .
Por conveniencia de la descripción, sea {x, y} = f{i-1, x \cdot w_i + y}. Entonces la regla de transición se puede escribir como:
Además, sea . Entonces la regla de transición se puede expresar como:
Esto se transforma en una forma clásica de optimización con cola monótona. se puede calcular en , así que para un fijo podemos calcular en tiempo . Por lo tanto, la complejidad de hallar todos los es . De este modo, la complejidad total del algoritmo se reduce a .
Mochila mixta (Mixed Knapsack)
El problema de la mochila mixta combina los tres problemas descritos arriba. Es decir, algunos ítems solo se pueden tomar una vez, algunos se pueden tomar infinitas veces, y algunos se pueden tomar a lo sumo veces.
El problema puede parecer intimidante, pero mientras se entiendan las ideas centrales de los problemas de mochila anteriores y se combinen, se puede resolver. El pseudocódigo de la solución es:
for (each item) {
if (0-1 knapsack)
Apply 0-1 knapsack code;
else if (complete knapsack)
Apply complete knapsack code;
else if (multiple knapsack)
Apply multiple knapsack code;
}