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