| @ -0,0 +1,144 @@ | |||
| In this post I will go over how you can frame a question in terms of recursion and then slowly massage the question into a dynamic programming question. | |||
| # Problem Description | |||
| The knapsack problem is a famous optimization problem in computer science. | |||
| Given a set of items, a thief has to determine which items he should steal to maximize his total profits. | |||
| The thief knows the maximum capacity of his knapsack and the weights and values of all the items. | |||
| The fractional knapsack problem is where a thief can steal fractions of the items instead of being mandated to take the whole item. | |||
| This problem can be solved really easily using a greedy algorithm where the thief just steals the most valuable dense objects first. | |||
| The 0-1 knapsack problem is where the thief can't steal fractions of items. | |||
| By nature this object is more difficult since it requires more computations. | |||
| A naive brute force algorithm would take exponential time, but, dynamic programming can slash the complexity. | |||
| # Recursive Formula | |||
| The initial recursive way of defining the problem will look something like this: | |||
| $$ | |||
| k([], W) = 0\\ | |||
| k((v,w)::items, W) = | |||
| \begin{cases} | |||
| k(items, W) & \text{if } w > W\\ | |||
| max(k(items, W), k(items, W- w) + v) & otherwise\\ | |||
| \end{cases} | |||
| $$ | |||
| In this example the knapsack problem takes in two parameters, a list of items and a max capacity of the knapsack. | |||
| We have three cases in this formulation. If the knapsack is empty, the max value we can steal is 0. | |||
| If the weight of the item at the start of the list is greater than the capacity of the knapsack, we can't steal it. | |||
| The final case is were we decide whether it would be better to take the item or not. | |||
| It is often nice to work with lists when theoretically formulating a recursive definition. | |||
| However, it is better to have arrays when actually coding the problem. | |||
| $$ | |||
| k(a, W) = | |||
| \begin{cases} | |||
| 0 & \text{if } |a| = 0\\ | |||
| k(a[1:], W) & \text{if } a[0].w > W\\ | |||
| max(k(a[1:], W), k(a[1:], W- a[0].w) + a[0].v) & otherwise\\ | |||
| \end{cases} | |||
| $$ | |||
| In this formulation we converted the list into a array. The array is filled with objects which has a "v" value and "w" weight property. | |||
| The [1:] syntax represents array slicing where you take everything after the first element. | |||
| Slicing is nice theoretically, but, in practice they are slow. | |||
| To stop using slicing we will have to introduce a slicing index to our formulation. | |||
| This will make all of our array operations constant time and prevent our memory from growing exponentially under recursive calls. | |||
| $$ | |||
| k'(a, W) = | |||
| \begin{cases} | |||
| 0 & \text{if } i = |a|\\ | |||
| k'(a, W, i+1) & \text{if } a[i].w > W\\ | |||
| max(k'(a, W, i+1), k'(a, W- a[i].w, i+1) + a[i].v) & otherwise\\ | |||
| \end{cases} | |||
| $$ | |||
| # Dynamic Programming Formulation | |||
| Now that we have a recursive definition which has overlapping sub problems, we can convert it to imperative pseudo code which uses dynamic programming. | |||
| Think of the two-dimensional array as a way to store the results of the recursive calls bottom up. | |||
| This will prevent us from having an exponential number of computations. | |||
| ````Python | |||
| def knapsack(a, W): | |||
| n = |a| | |||
| k = array(0...W, 0... n) | |||
| for w = 0 to W: # base case i = n | |||
| k[w, n] = 0 | |||
| for i = n-1 down to 0: | |||
| for w = 0 to W: | |||
| if a[i].w > W: # unable to take current item | |||
| k[w, i] = k[w, i+1] | |||
| else: #decides whether to include item | |||
| k[w, i] = max(k[w,i+1], k[w-a[i].w, i+1] + a[i].v) | |||
| return k[W, 0] | |||
| ```` | |||
| We can now easily implement the pseudo code in python. | |||
| Instead of just returning the maximum value which our thief can steal, we can return a list of objects which the thief actually stole. | |||
| ````Python | |||
| def knapsack(V, W, capacity): | |||
| """ | |||
| Dynamic programming implementation of the knapsack problem | |||
| :param V: List of the values | |||
| :param W: List of weights | |||
| :param capacity: max capacity of knapsack | |||
| :return: List of tuples of objects stolen in form (w, v) | |||
| """ | |||
| choices = [[[] for i in range(capacity + 1)] for j in range(len(V) + 1)] | |||
| cost = [[0 for i in range(capacity + 1)] for j in range(len(V) + 1)] | |||
| for i in range(0, len(V)): | |||
| for j in range(0, capacity + 1): | |||
| if W[i] > j: # don't include another item | |||
| cost[i][j] = cost[i -1][j] | |||
| choices[i][j] = choices[i - 1][j] | |||
| else: # Adding another item | |||
| cost[i][j] = max(cost[i-1][j], cost[i-1][j - W[i]] + V[i]) | |||
| if cost[i][j] != cost[i-1][j]: | |||
| choices[i][j] = choices[i - 1][j - W[i]] + [(W[i], V[i])] | |||
| else: | |||
| choices[i][j] = choices[i - 1][j] | |||
| return choices[len(V) -1][capacity] | |||
| def printSolution(S): | |||
| """ | |||
| Takes the output of the knapsack function and prints it in a | |||
| pretty format. | |||
| :param S: list of tuples representing items stolen | |||
| :return: None | |||
| """ | |||
| print("Thief Took:") | |||
| for i in S: | |||
| print("Weight: " + str(i[0]) + "\tValue: \t" + str(i[1])) | |||
| print() | |||
| print("Total Value Stolen: " + str(sum(int(v[0]) for v in S))) | |||
| print("Total Weight in knapsack: " + str(sum(int(v[1]) for v in S))) | |||
| values = [1,1,1,1,1,1] | |||
| weights = [1,2,3,4,5,1] | |||
| printSolution(knapsack(values, weights, 5)) | |||
| ```` | |||
| # Time Complexity | |||
| Since we simply calculate each value in our two dimensional array, the complexity is 0(n*W). | |||
| The W is the max capacity of the knapsack and the n is the number of objects you have. | |||
| The interesting caveat with this problem is that it is NP-Complete. | |||
| Notice that we have a solution which runs in "polynomial" time. | |||
| NP hard problems currently don't have any known polynomial solutions for them. | |||
| In our case here, we simply have a pseudo polynomial solution -- size required to solve solution grows exponentially with respect to input. | |||
| Since it has this pseudo polynomial solution, the knapsack is [weak NP-complete](https://en.wikipedia.org/wiki/Weak_NP-completeness). | |||