Dynamic Programming - 0/1 Knapsack.
Dynamic Programming has two characteristics: 1) overlapping subproblems 2) optimal substructure. Dynamic programming includes two methods: 1) top-down with memoization (recursion) 2) bottom-up with tabulation (no recursion).
1] Top down without memoization.
def Fibonacci(n):
if n < 2:
return n
return Fibonacci(n-1) + Fibonacci(n-2)
2] Top down with memoization.
def Fibonacci(n):
memoize = [-1 for _ in range(n+1)]
return recursion(memoize, n)
def recursion(memoize, n):
if n < 2:
return n
# if the subproblem has been solved
if memoize[n] >= 0:
return memoize[n]
# if the subproblem has not been solved
memoize[n] = recursion(memoize, n-1) + recursion(memoize, n-2)
return memoize[n]
3] Bottom up with tabulation.
def Fibonacci(n):
dp = [0,1]
for i in range(2, n + 1):
dp.append(dp[i-1] + dp[i-2])
return dp[n]
1. 0/1 Knapsack
Given two integer arrays to represent weights and profits of N items, we need to find a subset of these items which will give us maximum profit such that their cumulative weight is not more than a given number C. Write a function that returns the maximum profit. Each item can only be selected once, which means either we put an item in the knapsack or skip it.
1] Brute Force - Recursion.
# Time complexity O(2^N). Space complexity O(N).
def solve_knapsack(profits, weights, capacity):x
return recursion(profits, weights, capacity, 0)
def recursion(profits, weights, capacity, index):
# base
if capacity <=0 or index >= len(profits):
return 0
# recursion call after choosing the element at the index
profit1 = 0
if weights[index] <= capacity:
profit1 = profits[index] + recursion(profits, weights, capacity - weights[index], index + 1)
# recursion call after excluding the element at the index
profit2 = recursion(profits, weights, capacity, index + 1)
return max(profit1, profit2)
2] Top Down Dynamic Programming with Memoization. (recursion)
# Time complexity O(N * C). Space complexity O(N * C). N is the number of items and C is the capacity.
def solve_knapsack(profits, weights, capacity):
dp = [[-1 for _ in range(capacity + 1)] for _ in range(len(profits))]
return recursion(dp, profits, weights, capacity, 0)
def recursion(dp, profits, weights, capacity, index):
# base
if capacity <= 0 or index >= len(profits):
return 0
# recursion call after choosing the element at the index
profit1 = 0
if weights[index] <= capacity:
profit1 = profits[index] + recursion(dp, profits, weights, capacity - weights[index], index + 1)
# recursion call after excluding the element at the index
profit2 = recursion(dp, profits, weights, capacity, index + 1)
dp[index][capacity] = max(profit1, profit2)
return dp[index][capacity]
3] Bottom Up Dynamic Programming. (Optimal solution will be the maximum of the following two values dp[i][c] = max(dp[i-1][c], profits[i] + dp[i-1][c - weights[i]])
)
# Time complexity O(N * C). Space complexity O(N * C).
def solve_knapsack(profits, weights, capacity):
# base
n = len(profits)
if capacity <= 0 or n == 0 or len(weights) != n:
return 0
dp = [[0 for _ in range(capacity + 1)] for _ in range(n)]
# populate the capacity = 0 columns with 0 profits
for i in range(n):
dp[i][0] = 0
# populate the first row with the corresponding profit if the weight does not exceed the capacity
for c in range(capacity + 1):
if weights[0] <= c:
dp[0][c] = profits[0]
# process all sub array for all the capacities
for i in range(1,n):
for c in range(1, capacity + 1):
profit1, profit2 = 0, 0
# include the item, if it is not more than the capacity
if weights[i] <= c:
profit1 = profits[i] + dp[i-1][c - weights[i]]
# exclude the item
profit2 = dp[i-1][c]
# take the maximum
dp[i][c] = max(profit1, profit2)
# maximum profit will be at the right bottom corner
return dp[n-1][capacity]
Reduce the space complexity to O(C).
def solve_knapsack(profits, weights, capacity):
# base
n = len(profits)
if capacity <= 0 or n == 0 or len(weights) != n:
return 0
dp = [0 for _ in range(capacity + 1)]
for c in range(capacity + 1):
if weights[0] <= c:
dp[c] = profits[0]
for i in range(1, n):
for c in range(capacity, -1, -1): # reverse order
profit1, profit2 = 0, 0
# include the item
if weights[i] <= c:
profit1 = profits[i] + dp[c - weights[i]]
# exclude the item
profit2 = dp[c]
# choose the maximum
dp[c] = max(profit1, profit2)
return dp[capacity]
2. Equal Subset Sum Partition
Given a set of positive numbers, find if we can partition it into two subsets such that the sum of elements in both the subsets is equal.
1] Brute Force Recursion. Assume the total number of all the given numbers is ‘S’, then the two equal subsets must have.a sum equal to ‘S/2’. So we turn t o find a subset of teh given numbers that has a total sum of ‘S/2’.
# Time complexity O(2^N). Space complexity O(N)
def can_partition(num):
SUM = sum(num)
# SUM cannot be an odd number, otherwise two subsets cannot have equal sum.
if SUM % 2 != 0:
return False
return recursive(num, SUM / 2, 0)
def recursive(num, SUM, index):
# base
if SUM == 0:
return True
n = len(num)
if n == 0 or index >= n:
return False
# recursive call after choosing the number at the index
if num[index] <= SUM:
if recursive(num, SUM - num[index], index + 1):
return True
# recursive all after excluding teh number at the index
return recursive(num, SUM, index + 1)
2] Top-down Dynamic Programming with Memoization.