El problema de la mochila siempre se explica mejor por medio de un ejemplo.
Supongan que son un ladrón que acaba de entrar a una bóveda, para esto ustedes solo llevan consigo una mochila que tiene una capacidad limitada, en este caso de c cantidad de kilos. Ahora ante sus ojos se encuentran con n objetos cada uno con un valor v y un peso p.
Lamentablemente usted no puede llevar todos los objetos así que debe de escoger aquella combinación de objetos tal que la suma de sus valores sea máxima y la suma de sus pesos no rebase la capacidad de la mochila.
Una definición rigurosa la pueden leer en wikipedia.
Vamos a ver ejemplo mas simple de como resolver esto:
Antes de empezar definamos nuestra clase Item
Código Python:
No es mas que una simple clase llamada "Item" que contiene un valor y un peso. Ademas tiene una forma bonita para convertirlo a un string.Ver original
class Item: def __init__(self, value, weight): self.value = value self.weight = weight def __str__(self): return "Value: %d - Weight: %d" % (self.value, self.weight) def __repr__(self): return self.__str__()
Los datos con los que vamos a con los que vamos a estar trabajando son los siguientes:
Código Python:
Ver original
items = [Item(4, 12), Item(2, 2), Item(2, 1), Item(1, 1), Item(10, 4), ]
Fuerza bruta
Ahora la manera mas directa de resolver esto por por fuerza bruta esto es generar todas las combinaciones y ver que combinación o combinaciones nos convienen mas. De momento solo mostrare como obtener la mayor suma de los valores de los items sin sobre pasar la capacidad de la mochila.
Código Python:
Ver original
c = 0 def ks(index, weight): global items global c c += 1 if index >= len(items): return 0 item = items[index] if item.weight > weight: return ks(index + 1, weight) else: return max(ks(index + 1, weight), ks(index + 1, weight - item.weight) + item.value) print "Max sum: %d" % (ks(0, 20),) print "Iterations %d" % (c,)
Linea 1: Es solo un contador de llamadas a función
Linea 2: Es la definición de nuestra función, que toma como parámetros un indice "index" y un peso o capacidad de la mochila.
Linea 3 y 4: Pones al alcance variables globales como "items" y "c"
Linea 5: Incrementamos el contador de llamadas
Linea 7 y 8: Checamos si el indice se encuentra dentro del limite de la lista de items, si no entonces no podemos agarrar ningún item y regresamos 0.
Linea 10: Ponemos item como el item en la posición del indice dado.
Linea 12 y 16: Verificamos si podemos poner el item dentro de la mochila, si no podemos entonces simplemente pasamos al siguiente item. Si podemos meter el item a la mochila entonces podemos agarrar 2 caminos meter el item y esto involucra varias cosas primero la capacidad de mochila se reduce, segundo el valor de los items agarrados hasta el momento aumenta pero también puede que sea una mala decisión y que el espacio que ocupa ese item puede ser usado por otro u otros items que sumen un mayor valor. Como nosotros no sabes cual es el mejor camino entonces tomamos los 2 caminos y simplemente agarramos el que camino que tenga mayor valor.
El resto de las lineas solo imprime la suma máxima, suponiendo que la capacidad de la mochila es de 20kg y despues imprime cuantas veces se llamo a esa función.
La complejidad de este algoritmo es de O(2^n) donde n es la cantidad de items, entonces se puede decir que este algoritmo tiene una complejidad exponencial.
Si graficamos el árbol de llamadas a función nos vamos a topar con que muchas llamadas a función se repiten, después de todo si llamamos 2^n veces a la función pero nuestro espacio de parámetros es de tan solo n * m donde n es el numero de items y m es el peso entonces podemos concluir que muchas llamadas a función se repetirán.
Programación dinámica (recursiva)
Entonces para optimizar este algoritmo vamos a guardar las llamadas a función que hagamos con una técnica llamada memoization.
Entonces nuestro algoritmo queda mas o menos de la siguiente manera:
Código Python:
La lógica sigue siendo la misma. Lo único que cambia es que añadi memoization para cada llamada a función.Ver original
c = 0 mem = {} def ks(index, weight): global items global c global mem c += 1 case = (index, weight) if case in mem: return mem[case] if index >= len(items): mem[case] = 0 return mem[case] item = items[index] grab_case = (index + 1, weight - item.weight) no_grab_case = (index + 1, weight) if item.weight > weight: mem[case] = ks(*no_grab_case) return mem[case] else: if no_grab_case not in mem: mem[no_grab_case] = ks(index + 1, weight) if grab_case not in mem: mem[grab_case] = ks(index + 1, weight - item.weight) + item.value mem[case] = max(mem[grab_case], mem[no_grab_case]) return mem[case] print "Max sum: %d" % (ks(0, 20),) print "Iterations %d" % (c,)
Y definí tuplas como case que indica el caso actual, grab_case el caso donde agarramos el item y no_grab_case el caso donde no agarramos el item.
Si se fijan el resultado es el mismo, pero con menos llamadas a función. La complejidad de este algoritmo se ha reducido a O(n * m)
Programación dinámica (Iterativa)
Ahora para saber que items tenemos que tomar es necesario mantener una matriz de items que hemos tomado, en la fila va el item y en las columnas el paso y ademas tenemos que tener otra matriz con todas los valores de las llamadas a función.
Código Python:
Ver original
def ks(items, weight): mem = [[0 for j in xrange(weight + 1)] for i in xrange(len(items) + 1)] grab = [[0 for j in xrange(weight + 1)] for i in xrange(len(items) + 1)] for i, item in enumerate(items, start=1): for j in xrange(1, weight + 1): if item.weight <= j: if item.value + mem[i][j - item.weight] >= mem[i - 1][j]: mem[i][j] = item.value + mem[i][j - item.weight] grab[i][j] = 1 else: mem[i][j] = mem[i - 1][j] else: mem[i][j] = mem[i - 1][j] itemList = [] n = len(items) while n > 0 and weight >= 0: if grab[n][weight]: itemList.append(items[n-1]) weight -= items[n-1].weight n -= 1 return itemList print ks(items, 15)
Existen soluciones triviales por ejemplo si no tengo ningún item a tomar entonces la mejor suma es 0 o si la capacidad de mi mochila es 0 entonces la mejor suma también es 0.
En la linea 1: Definimos la función que regresa una lista de items tal que sus valores sumados sea máximo y sus pesos sumados no rebasen la capacidad de la mochila. Y toma como parámetros una lista de items y la capacidad de la mochila.
Linea 2 a la 6: Creamos 2 matrices una para guardar las mejores sumas y otra para guardad que elementos hemos tomado.
Linea 9 a 18: Es donde ocurre la magia, iteramos sobre todos los items y pesos mayores i iguales a 1. Y por cada iteracion vamos resolviendo un mini-problema de la mochila que nos ayudara a resolver un problema de la mochila mas grande. Recuerda que las filas indican el ítem y que las columnas indican la capacidad de la mochila. Por lo que la fila (0, i) y (i, 0) están inicialmente en 0 por que o no tenemos items o no tenemos capacidad en la mochila.
Así que la lógica sigue siendo mas o menos la misma.
Linea 11, 17, 18: Preguntamos si podemos meter ese ítem a la mochila, si no podemos meter ese item a la mochila entonces la mejor solución es no tomar el ítem y por lo tanto agarramos lo mejor que podemos hacer con ese espacio no tomando ese item.
Linea 11: En adelante si podemos tomar el ítem entonces tenemos 2 caminos, tomar o no tomar el ítem. En este caso ambos caminos ya los hemos recorrido así que solo tenemos que checar sí el valor del ítem mas la mejor decisión con el peso restante es mayor o igual al valor de no tomar el ítem. Si es así entonces ponemos marcamos en la matriz que hemos tomado ese elemento y ponemos como solución el valor del ítem mas la mejor decisión con la capacidad restante de lo contrario ponemos como solución el caso donde hemos tomado el ítem.
Linea 20 a 27: Tenemos la parte donde obtenemos los items que formar la mejor combinación. Empezamos desde el ultimo ítem hasta el primer ítem y verificamos si esta marcado en la matriz de agarrados ademas la capacidad de la mochila va disminuyendo conforme tomemos un ítem.