| @ -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). | |||||