Dynamic Programming#
Knapsack#
[3]:
from typing import List
[4]:
def knapsack(wt, val, max_weight, n, memo):
if memo[max_weight][n] is not None:
return memo[max_weight][n]
# creates a recursion tree of take or no take
# base case if as we are starting with n - 1
if n == 0: # last element
# if we can take then
if wt[n] <= max_weight:
return val[n]
else:
return 0
# two options now - take or not take
# if not take then wight of bag will remain same
# and we will go to the next index
not_take = 0 + knapsack(wt, val, max_weight, n - 1, memo)
# if take then
take = float("-inf")
# check if we can take or not
if wt[n] <= max_weight:
# then pick val + (reduce weight and next index)
take = val[n] + knapsack(wt, val, max_weight - wt[n], n - 1, memo)
# max of take and no take
val = max(take, not_take)
memo[max_weight][n] = val
return memo[max_weight][n]
wt = [3, 2, 6]
val = [30, 40, 60]
n = len(wt)
max_weight = 6
memo = [[None for _ in range(n)] for _ in range(max_weight + 1)]
print(knapsack(wt, val, max_weight, n - 1, memo))
70
[5]:
# stop suggestions fucking stupid
def knapsack(wt, val, max_weight):
size = len(wt)
# rows idx of weights, cols idx of max weight
dp = [[0 for _ in range(max_weight + 1)] for _ in range(size)]
print(dp)
# bottom up approach
# loop wt idx 0 -> size - 1 and weight 0 -> max_weight
# base case
# if wt idx = 0 and if wt is less than max weight then I can pick
for weight in range(max_weight + 1):
if wt[0] <= weight:
dp[0][weight] = val[0]
# now itertively
for wt_idx in range(1, size):
for weight in range(0, max_weight + 1):
# wt_idx , weight
# 1, 0
# 1, 1
# 1, 2
# ...
# 2, 0
# 2, 1
# ...
# now two options take or no take
# if no take (no need to compare weight) -> no value is added + going to prev index with same weight
print(wt_idx, weight, wt_idx - 1)
no_take = 0 + dp[wt_idx - 1][weight]
# if take
# because we need maximum
take = float("-inf")
# check ig weight is less then wt idx
if wt[wt_idx] <= weight:
# then take and value is added + weight is reduced and prev idx
take = val[wt_idx] + dp[wt_idx - 1][weight - wt[wt_idx]]
dp[wt_idx][weight] = max(take, no_take)
return dp[size - 1][max_weight]
wt = [3, 2, 6]
val = [30, 40, 60]
max_weight = 6
print(knapsack(wt, val, max_weight))
[[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]]
1 0 0
1 1 0
1 2 0
1 3 0
1 4 0
1 5 0
1 6 0
2 0 1
2 1 1
2 2 1
2 3 1
2 4 1
2 5 1
2 6 1
70
fibonacci#
1 1 2 3 5 8 13
[6]:
def fib_1(n):
if n <= 1:
return n
return fib_1(n-2) + fib_1(n-1)
print(fib_1(7))
13
[7]:
%timeit fib_1(7)
2.07 μs ± 283 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
[8]:
def fib_2(n, cache):
if n <= 1:
return n
if cache[n] is not None:
return cache[n]
else:
val = fib_2(n-2, cache) + fib_2(n-1, cache)
cache[n] = val
return val
n = 7
cache = [None] * (n + 1)
fib_2(7, cache)
[8]:
13
[9]:
%timeit fib_2(7, cache=[None] * 8)
980 ns ± 53.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
[ ]:
def fib_3(n):
dp = [1, 1]
for i in range(2, n):
val = dp[i - 1] + dp[i - 2]
dp.append(val)
return dp[-1]
fib_3(7)
13
[11]:
%timeit fib_3(7)
458 ns ± 46.6 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
Longest Common Subsequence#
[12]:
def longest_common_subsequence(text1_idx, text2_idx, text1, text2, memo):
# anything goes negative nothing will match
if text1_idx < 0 or text2_idx < 0:
return 0
if memo[text1_idx][text2_idx] is not None:
return memo[text1_idx][text2_idx]
if text1[text1_idx] == text2[text2_idx]:
# if match both idxes are decreased simultaneously
memo[text1_idx][text2_idx] = 1 + longest_common_subsequence(
text1_idx - 1, text2_idx - 1, text1, text2, memo
)
else:
# if not match both idxes are decreased one by one and max of both independent results is taken
memo[text1_idx][text2_idx] = max(
longest_common_subsequence(text1_idx - 1, text2_idx, text1, text2, memo),
longest_common_subsequence(text1_idx, text2_idx - 1, text1, text2, memo),
)
print(memo)
return memo[text1_idx][text2_idx]
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
text1_size = len(text1)
text2_size = len(text2)
memo = [[None for _ in range(text2_size)] for _ in range(text1_size)]
return longest_common_subsequence(
text1_size - 1, text2_size - 1, text1, text2, memo
)
Solution().longestCommonSubsequence("abcde", "ace")
[[1, None, None], [None, None, None], [None, None, None], [None, None, None], [None, None, None]]
[[1, None, None], [1, None, None], [None, None, None], [None, None, None], [None, None, None]]
[[1, None, None], [1, None, None], [None, 2, None], [None, None, None], [None, None, None]]
[[1, None, None], [1, None, None], [1, 2, None], [None, None, None], [None, None, None]]
[[1, None, None], [1, None, None], [1, 2, None], [1, None, None], [None, None, None]]
[[1, None, None], [1, None, None], [1, 2, None], [1, 2, None], [None, None, None]]
[[1, None, None], [1, None, None], [1, 2, None], [1, 2, None], [None, None, 3]]
[12]:
3
[13]:
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
text1_size = len(text1)
text2_size = len(text2)
dp = [[0 for _ in range(text2_size + 1)] for _ in range(text1_size + 1)]
lcs = ""
for t1_idx in range(1, text1_size + 1):
for t2_idx in range(1, text2_size + 1):
print(text1[t1_idx - 1], text2[t2_idx - 1])
if text1[t1_idx - 1] == text2[t2_idx - 1]:
dp[t1_idx][t2_idx] = 1 + dp[t1_idx - 1][t2_idx - 1]
lcs += text1[t1_idx - 1]
else:
dp[t1_idx][t2_idx] = max(
dp[t1_idx][t2_idx - 1], dp[t1_idx - 1][t2_idx]
)
print(dp, lcs)
return dp[text1_size][text2_size]
Solution().longestCommonSubsequence("abcde", "ace")
a a
a c
a e
b a
b c
b e
c a
c c
c e
d a
d c
d e
e a
e c
e e
[[0, 0, 0, 0], [0, 1, 1, 1], [0, 1, 1, 1], [0, 1, 2, 2], [0, 1, 2, 2], [0, 1, 2, 3]] ace
[13]:
3
Longest Common Substring#
[14]:
def longest_common_substring(text1, text2, text1_idx, text2_idx, l):
if text1_idx < 0 or text2_idx < 0:
return []
if text1[text1_idx] == text2[text2_idx]:
l.append(text1[text1_idx])
longest_common_substring(text1, text2, text1_idx - 1, text2_idx - 1, l)
return l
l = max(
longest_common_substring(text1, text2, text1_idx - 1, text2_idx, []),
longest_common_substring(text1, text2, text1_idx, text2_idx - 1, []),
)
return l
class Solution:
def longestCommonSubstring(self, text1: str, text2: str) -> int:
text1_size = len(text1)
text2_size = len(text2)
return longest_common_substring(
text1, text2, text1_size - 1, text2_size - 1, []
)[::-1]
Solution().longestCommonSubstring("acd", "lcde")
[14]:
['c', 'd']
Climb Stairs#
[15]:
def climb_stairs(n, memo):
if n < 0:
return 0
if n == 0:
return 1
if n == 1:
return 1
if n == 2:
return 2
if memo[n] is not None:
return memo[n]
memo[n] = climb_stairs(n - 1) + climb_stairs(n - 2)
return memo[n]
class Solution:
def climbStairs(self, n: int) -> int:
memo = [None] * (n + 1)
return climb_stairs(n, memo)
Solution().climbStairs(2)
[15]:
2
[16]:
class Solution:
def climbStairs(self, n: int) -> int:
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
dp[2] = 2
for idx in range(3, n):
dp[idx] = dp[idx] + dp[idx - 1] + dp[idx - 2]
return dp[n]
Solution().climbStairs(2)
[16]:
2
Longest Increasing Subsequence#
[17]:
from typing import List
def longest_increasing_subseq(nums, size, idx, prev_idx, memo):
# base case -> increasing idx till the end of the array
# nothing else to compute
if idx == size:
return 0
if memo[idx][prev_idx + 1] is not None:
return memo[idx][prev_idx + 1]
# if i dont take then idx will increase but prev idx will remain same as we have not picked
no_take = 0 + longest_increasing_subseq(nums, size, idx + 1, prev_idx, memo)
# if take
# check condition (are we at the first digit /prev == -1 or the prev value is less than current value)
take = float("-inf")
if prev_idx == -1 or nums[prev_idx] < nums[idx]:
# len = 1 + (idx is incremented and current idx becomes prev idx)
take = 1 + longest_increasing_subseq(nums, size, idx + 1, idx, memo)
val = max(no_take, take)
memo[idx][prev_idx + 1] = val
return memo[idx][prev_idx + 1]
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
size = len(nums)
# need to keep memo for both idx and prev idx
# now that we are starting with -1 memo will be accessed by prev idx + 1
memo = [[None for _ in range(size + 1)] for _ in range(size)]
return longest_increasing_subseq(nums, size, 0, -1, memo)
Solution().lengthOfLIS(nums=[10, 9, 2, 5, 3, 7, 101, 18])
[17]:
4
[18]:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
size = len(nums)
# need to keep memo for both idx and prev idx
# now that we are starting with -1 memo will be accessed by prev idx + 1
dp = [[0 for _ in range(size + 1)] for _ in range(size)]
for idx in range(size - 1, -1, -1):
for prev_idx in range(idx - 1, -2, -1):
no_take = 0 + dp[idx][prev_idx + 1]
take = float("-inf")
if prev_idx == -1 or nums[prev_idx] < nums[idx]:
take = 1 + dp[idx][idx + 1]
dp[idx][prev_idx + 1] = max(take, no_take)
print(dp)
return dp[-1][size - 1]
Solution().lengthOfLIS(nums=[10, 9, 2, 5, 3, 7, 101, 18])
[[1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0, 0, 0, 0], [1, 0, 0, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 1, 1, 1, 0, 0]]
[18]:
0
Coin Change II#
[19]:
def coin_change_ii(coins, index, amount, memo):
if amount == 0:
return 1
if amount < 0:
return 0
if (index, amount) in memo:
return memo[(index, amount)]
solution = 0
n_coins = len(coins)
for i in range(index, n_coins):
solution = solution + coin_change_ii(coins, i, amount - coins[i], memo)
memo[(index, amount)] = solution
return solution
memo = {}
coin_change_ii([2, 2340, 4680], 0, 4681, memo)
[19]:
0
[20]:
def coin_change_ii(coins, amount):
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for am in range(coin, amount + 1):
print(am, am - coin, dp)
dp[am] += dp[am - coin]
return dp[amount]
coin_change_ii([1, 2, 5], 6)
1 0 [1, 0, 0, 0, 0, 0, 0]
2 1 [1, 1, 0, 0, 0, 0, 0]
3 2 [1, 1, 1, 0, 0, 0, 0]
4 3 [1, 1, 1, 1, 0, 0, 0]
5 4 [1, 1, 1, 1, 1, 0, 0]
6 5 [1, 1, 1, 1, 1, 1, 0]
2 0 [1, 1, 1, 1, 1, 1, 1]
3 1 [1, 1, 2, 1, 1, 1, 1]
4 2 [1, 1, 2, 2, 1, 1, 1]
5 3 [1, 1, 2, 2, 3, 1, 1]
6 4 [1, 1, 2, 2, 3, 3, 1]
5 0 [1, 1, 2, 2, 3, 3, 4]
6 1 [1, 1, 2, 2, 3, 4, 4]
[20]:
5
Length of longest substring#
[21]:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
size = len(s)
if size == 0:
return 0
if size == 1:
return 1
max_counter = 0
for slow_idx in range(0, size):
hashmap = set(s[slow_idx])
for fast_idx in range(slow_idx + 1, size):
if s[fast_idx] in hashmap:
break
else:
hashmap.add(s[fast_idx])
max_counter = max(max_counter, len(hashmap))
return max_counter
Solution().lengthOfLongestSubstring("au")
[21]:
2
[22]:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
size = len(s)
if size == 0:
return 0
if size == 1:
return 1
hashmap = {}
slow_idx = 0
fast_idx = 0
max_len = 0
while fast_idx < size:
if s[fast_idx] not in hashmap:
hashmap[s[fast_idx]] = fast_idx
else:
if hashmap[s[fast_idx]] + 1 > slow_idx:
slow_idx = hashmap[s[fast_idx]] + 1
hashmap[s[fast_idx]] = fast_idx
max_len = max(fast_idx - slow_idx + 1, max_len)
fast_idx += 1
return max_len
Solution().lengthOfLongestSubstring("tmmzuxt")
[22]:
5
Maximum Subarray#
[23]:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
size = len(nums)
max_sum = float("-inf")
temp_sum = 0
for idx in range(size):
temp_sum += nums[idx]
if temp_sum <= 0:
temp_sum = 0
max_sum = max(max_sum, temp_sum)
return max_sum
Solution().maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])
[23]:
6
Minimum total Triangle#
[24]:
from typing import List
def minimum_total(triangle, size, row_idx, col_idx, memo):
if row_idx == size - 1:
return triangle[row_idx][col_idx]
if memo[row_idx][col_idx] != -1:
return memo[row_idx][col_idx]
down = triangle[row_idx][col_idx] + minimum_total(
triangle, size, row_idx + 1, col_idx, memo
)
diagonal = triangle[row_idx][col_idx] + minimum_total(
triangle, size, row_idx + 1, col_idx + 1, memo
)
memo[row_idx][col_idx] = min(down, diagonal)
return memo[row_idx][col_idx]
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
size = len(triangle)
memo = [[-1 for _ in range(size)] for _ in range(size)]
return minimum_total(triangle, size, 0, 0, memo)
triangle = [[-1], [-2, -3], [-4, -5, -6]]
print(Solution().minimumTotal(triangle))
triangle = [[1], [-2, 3], [-1, 4, -5]]
print(Solution().minimumTotal(triangle))
triangle = [[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]
print(Solution().minimumTotal(triangle))
-10
-2
11