Optimisation: Dynamic Programming and Greedy¶
Table of Contents
Dynamic Programming¶
Expand Code
1def uniquePaths(self, m: int, n: int) -> int: 2 def grid(): 3 dp = [[0] * n for _ in range(m)] 4 for i in range(m): 5 dp[i][0] = 1 6 for j in range(n): 7 dp[0][j] = 1 8 for i in range(1, m): 9 for j in range(1, n): 10 dp[i][j] = dp[i-1][j] + dp[i][j-1] 11 return dp[-1][-1] 12 13 def optimized(): 14 # f(i,j) = f(i-1,j) + f(i,j-1) 15 # remove the first diemsion 16 # f(j) = f(j) + f(j-1) <- as long as f(j-1) is from the current iteration 17 # and f(j) is from the previous iteration 18 # so we need to fill this from left to right 19 dp = [1] * n 20 for i in range(1, m): 21 for j in range(1, n): 22 dp[j] = dp[j] + dp[j-1] 23 return dp[-1] 24 25 return optimized() 26 27def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int: 28 # dp[i][j] = 0 if obstacle 29 # dp[i][j] = dp[i-1][j] + dp[i][j-1] 30 # we can remove first dimension as it only depends on i-1 31 # dp[j] = dp[j] (from previous row) + dp[j-1] (current row) 32 # we need to fill it from left to right 33 # initialization: if there is an obstacle in the first row, 34 # all other cells after that would be unreachable 35 m, n = len(obstacleGrid), len(obstacleGrid[0]) if len(obstacleGrid) > 0 else 0 36 dp = [0] * n 37 j = 0 38 while j < n and not obstacleGrid[0][j]: 39 dp[j] = 1 40 j += 1 41 for i in range(1, m): 42 # this step is very important 43 # we not only need to check for obstacles in the current cell, but also previous cell 44 dp[0] = 1 if not obstacleGrid[i][0] and dp[0] else 0 45 for j in range(1, n): 46 dp[j] = dp[j] + dp[j-1] if not obstacleGrid[i][j] else 0 47 return dp[-1]
Expand Code
1def lcs(): 2 # say, we have sene i characters from text1 and j characters from text2 3 # the lcs(i, j) contains the longest common subsequence. 4 # if text1[i+1] == text2[j+1] then we increase lcs by 1 5 # else lcs is the max of lcs(i, j), lcs(i+1, j) and lcs(i, j+1) 6 # in the end, the final lcs is found at lcs(m, n) 7 m, n = len(text1), len(text2) 8 # dp table 9 lcs = [[0] * (n+1) for _ in range(m+1)] 10 # for every lcs(i+1, j+1), we need lcs(i, j), lcs(i+1, j) and lcs(i, j+1) 11 # that is, previous row and previous column of the same row 12 # so we have to fill the array from left to right, top to bottom 13 for i in range(m): 14 for j in range(n): 15 if text1[i] == text2[j]: 16 lcs[i+1][j+1] = lcs[i][j] + 1 17 else: 18 lcs[i+1][j+1] = max(lcs[i+1][j], lcs[i][j+1]) 19 20 return lcs[m][n] 21 22def lcs_memory_optimized(): 23 # f(i,j) = length of lcs from s[0..i-1] and t[0...j-1] 24 # transition rule: 25 # f(i+1,j+1) = f(i,j) + 1 if s[i] = t[j] 26 # f(i+1,j+1) = max(f(i+1,j), f(i,j+1)) 27 """ IMPORTANT THIS """ 28 # cannot be reduced to one dimension 29 # f(j+1) = f(j) + 1 if s[i] == t[j] (need f(j) from previous iteration) 30 # f(j+1) = f(j) (current iteration) + f(j+1) (previous iteration) 31 # need to keep track of the previous row 32 m, n = len(s), len(t) 33 dp_prev, dp = [0] * (n + 1), [0] * (n + 1) 34 for i in range(m): 35 for j in range(n): 36 if s[i] == t[j]: 37 dp[j+1] = dp_prev[j] + 1 38 else: 39 dp[j+1] = max(dp[j], dp_prev[j+1]) 40 dp_prev = dp[:] 41 return dp[-1] 42 43def construct(stack, indicators, i, j): 44 if i == 0 or j == 0: 45 return 46 if indicators[i][j] == 1: 47 construct(stack, indicators, i-1, j-1) 48 # need to keep this in mind that we only add characters in this case 49 stack.append(text1[i-1]) 50 elif indicators[i][j] == 2: 51 construct(stack, indicators, i-1, j) 52 else: 53 construct(stack, indicators, i, j-1) 54 55def lcs_str() -> str: 56 # need to keep additional storage for longest common subsequence string 57 # just need to store an indicator: 58 # 0 means it came from same row, previous column 59 # 1 means it came from previous row, previous column 60 # 2 means it came from previous row, same column 61 # also need to keep track of the index?? 62 m, n = len(text1), len(text2) 63 prev_row, curr_row = [0] * (n+1), [0] * (n+1) 64 indicators = [[0] * (n+1) for _ in range(m+1)] 65 66 for i in range(m): 67 for j in range(n): 68 if text1[i] == text2[j]: 69 curr_row[j+1] = prev_row[j] + 1 70 indicators[i+1][j+1] = 1 71 else: 72 if prev_row[j+1] < curr_row[j]: 73 curr_row[j+1] = curr_row[j] 74 indicators[i+1][j+1] = 0 75 else: 76 curr_row[j+1] = prev_row[j+1] 77 indicators[i+1][j+1] = 2 78 79 # copy current row to previous 80 for k in range(len(curr_row)): 81 prev_row[k] = curr_row[k] 82 83 stack = [] 84 construct(stack, indicators, m, n) 85 return ''.join(stack)
Expand Code
1def use_patience_solitaire(): 2 # we take the numbers as cards and deal them one by one to create decks 3 # (a) for a card, check the decks from left to right 4 # (b) if the card is smaller than the card on that deck, it goes on top 5 # (c) else we create a new deck with the card 6 # Note: 7 # (a) if we form the decks this way, the within a given deck, the numbers are 8 # arranged in decreasing order 9 # (b) the top card of the decks would be in increasing order from left to right 10 # (c) the number of decks created this way is the length of longest increasing subsequence 11 decks = [nums[0]] 12 for num in nums[1:]: 13 if decks and decks[-1] < num: 14 decks.append(num) 15 else: 16 insert_index = bisect_left(decks, num) 17 decks[insert_index] = num 18 return len(decks) 19 20def use_dp(): 21 # lis[i] is the longest increasing subsequence ending at nums[i] 22 # once I see nums[i+1], I need to consider all previous lis[k] 23 # instances k <= i, where nums[k] < nums[i+1]. The new number is 24 # able to create an lis of an increased length by 1 25 # base case: every lis[i] is at least 1 26 n = len(nums) 27 dp = [1] * n 28 max_len = 1 29 for i in range(1, n): 30 for k in range(i): 31 if nums[k] < nums[i]: 32 dp[i] = max(dp[i], dp[k] + 1) 33 max_len = max(max_len, dp[i]) 34 return max_len
Expand Code
1def longestPalindromeSubseq(self, s: str) -> int: 2 def lps(s): 3 # f(i,j) = length of lps after seeing s[i+1...j-1] 4 # need to compute f(i-1,j+1) after seeing s[i] and s[j] 5 # transition rule: 6 # f(i-1,j+1) = f(i,j) + 2 if s[i] == s[j] 7 # f(i-1,j+1) = max(f(i-1,j), f(i,j+1)) otherwise 8 # normalized equations 9 # f(i,j) = f(i+1,j-1) + 2 if s[i] == s[j] 10 # f(i,j) = max(f(i,j-1), f(i+1,j)) otherwise 11 # need to fill rows from bottom to top 12 # cannot remove dimension i 13 # when state (i,j) is updated, we need to know 14 # j-1 from next row 15 # j-1 from current row and j from next row 16 # so we need to keep track of the previous row 17 n = len(s) 18 dp_prev, dp = [0] * n, [0] * n 19 for i in range(n-1, -1, -1): 20 dp[i] = 1 # important because s[i..i] is a palindrome 21 for j in range(i+1, n): 22 if s[i] == s[j]: 23 dp[j] = dp_prev[j-1] + 2 24 else: 25 dp[j] = max(dp[j-1], dp_prev[j]) 26 dp_prev = dp[:] 27 return dp[-1] 28 29 def lcs(s, t): 30 # f(i,j) = length of lcs from s[0..i-1] and t[0...j-1] 31 # need to compute f(i+1,j+1) after seeing s[i] and t[j] 32 # transition rule: 33 # f(i+1,j+1) = f(i,j) + 1 if s[i] = t[j] 34 # f(i+1,j+1) = max(f(i+1,j), f(i,j+1)) 35 """ IMPORTANT THIS """ 36 # cannot be reduced to one dimension 37 # f(j+1) = f(j) + 1 if s[i] == t[j] (need f(j) from previous iteration) 38 # f(j+1) = f(j) (current iteration) + f(j+1) (previous iteration) 39 # need to keep track of the previous row 40 m, n = len(s), len(t) 41 dp_prev, dp = [0] * (n + 1), [0] * (n + 1) 42 for i in range(m): 43 for j in range(n): 44 if s[i] == t[j]: 45 dp[j+1] = dp_prev[j] + 1 46 else: 47 dp[j+1] = max(dp[j], dp_prev[j+1]) 48 dp_prev = dp[:] 49 return dp[-1] 50 51def longestPalindromeSubstr(self, s: str) -> int: 52 def lps(s): 53 # f(i,j) = length of lps after seeing s[i+1...j-1] 54 # need to compute f(i-1,j+1) after seeing s[i] and s[j] 55 # transition rule: 56 # f(i-1,j+1) = f(i,j) + 2 if s[i] == s[j] and f(i,j) = len(s[i+1...j-1]) 57 # f(i-1,j+1) = max(f(i-1,j), f(i,j+1)) otherwise 58 # normalized equations 59 # f(i,j) = f(i+1,j-1) + 2 if s[i] == s[j] 60 # f(i,j) = max(f(i,j-1), f(i+1,j)) otherwise 61 # need to fill rows from bottom to top 62 # cannot remove dimension i 63 # when state (i,j) is updated, we need to know 64 # j-1 from next row 65 # j-1 from current row and j from next row 66 # so we need to keep track of the previous row 67 n = len(s) 68 dp_prev, dp = [0] * n, [0] * n 69 start, size = 0, 1 70 for i in range(n-1, -1, -1): 71 dp[i] = 1 72 for j in range(i+1, n): 73 if s[i] == s[j] and dp_prev[j-1] == j-1-i: 74 dp[j] = dp_prev[j-1] + 2 75 if dp[j] > size: 76 start = i 77 size = dp[j] 78 else: 79 dp[j] = max(dp[j-1], dp_prev[j]) 80 dp_prev = dp[:] 81 return s[start:start + size] 82 return lps(s)
Knapsack
[leetcode.com] LC Discuss Post
[cp-algorithms.com] Knapsack Problem
[Similar to Bounded Knapsack] Count Ways to Target Sum
Expand Code
1from functools import cache 2 3def findTargetSumWays(self, nums: List[int], target: int) -> int: 4 """ 5 IMPORTANT!! NEED TO KEEP IN MIND THAT RUNNING SUM HAS TO BE IN THE STATE. 6 WE CANNOT MEMOIZE/USE DP IF WE KEEP THE RECURSIVE CALL STRUCTURE REDUCING 7 TARGET. THAT METHOD IS NOT STATELESS AS COUNT CHANGES ACROSS CALLS. 8 """ 9 def bounded_knapsack(): 10 # f(i, j) = number of ways to obtain target j after seeing i nums 11 # f(0, 0) = 1 12 # f(i+1, j) = f(i, j-nums[i]) + f(i, j+nums[i]) 13 # update rule: 14 # every f(i, j) would update f(i+1, j-nums[i]) and f(i+1, j+nums[i]) 15 """ 16 This is super important because if we loop the other way, we'd miss out on many updates. 17 """ 18 # range for j: [-sum(abs(nums)), sum(abs(nums))] 19 # range for i: 0...n-1 20 # gotta be careful with offset 21 n = len(nums) 22 max_sum = sum(nums) 23 if target > max_sum or target < -max_sum: 24 return 0 25 max_range = 2 * max_sum + 1 26 dp = [[0] * max_range for _ in range(n + 1)] 27 # dp[-][max_sum] actually represents curr sum = 0 28 dp[0][max_sum] = 1 # important 29 for i in range(n): 30 """ 31 Incorrect way of doing it: 32 for j in range(nums[i], max_range - nums[i]): 33 dp[i+1][j] = dp[i][j-nums[i]] + dp[i][j+nums[i]] 34 """ 35 # deciding on the range of j 36 for j in range(nums[i], max_range - nums[i]): 37 dp[i+1][j-nums[i]] += dp[i][j] 38 dp[i+1][j+nums[i]] += dp[i][j] 39 return dp[-1][max_sum + target] 40 41 def memoized_with_annotation(): 42 @cache 43 def dfs(index, curr_sum): 44 if index == len(nums): 45 return 1 if curr_sum == target else 0 46 sub_curr = dfs(index + 1, curr_sum - nums[index]) 47 add_curr = dfs(index + 1, curr_sum + nums[index]) 48 return sub_curr + add_curr 49 return dfs(0, 0) 50 51 def memoized(): 52 # the idea is - every time we reach target 0 after seeing n numbers, we increase the count 53 # memo dimension: index X max_range 54 # note that the state here is current_sum as opposed to target 55 # the reason is - to make the function calls stateless, we cannot use 56 # reduced target as every time we call the same target the overall count 57 # increases. on the other hand, current sum with the same index remains same 58 # and hence can be memoized 59 # range of curr_sum: 2*max_sum + 1 60 # max_sum = 10, range: [-10,...-1, 0, 1,...10] 61 # index: [ 0, 9,10,11, 21] 62 max_sum = sum([abs(num) for num in nums]) 63 max_range = 2 * max_sum + 1 64 memo = [[-1] * max_range for _ in range(len(nums))] 65 66 def dfs(index, curr_sum): 67 # not storing index=n entries because we only need 1 68 if index == len(nums): 69 return 1 if curr_sum == target else 0 70 curr_sum_index = curr_sum + max_sum 71 if memo[index][curr_sum_index] == -1: 72 sub_curr = dfs(index + 1, curr_sum - nums[index]) 73 add_curr = dfs(index + 1, curr_sum + nums[index]) 74 memo[index][curr_sum_index] = sub_curr + add_curr 75 return memo[index][curr_sum_index] 76 77 return dfs(0, 0) 78 79 def recursive_stateless(): 80 def dfs(index, curr_sum): 81 if index == len(nums): 82 return 1 if curr_sum == target else 0 83 sub_curr = dfs(index + 1, curr_sum - nums[index]) 84 add_curr = dfs(index + 1, curr_sum + nums[index]) 85 return sub_curr + add_curr 86 return dfs(0, 0) 87 88 def recursive_with_external_variable(): 89 count = 0 90 def dfs(index, curr_sum): 91 nonlocal count 92 if index == len(nums): # reached the end 93 if curr_sum == target: # achieved the target 94 count += 1 95 else: 96 dfs(index + 1, curr_sum - nums[index]) 97 dfs(index + 1, curr_sum + nums[index]) 98 # no need to do anything here as the state is updated on 99 # reaching the target to the external variable 100 dfs(0, 0) 101 return count 102 return bounded_knapsack()Unbounded Fixed Target Sum [Backtracking]
Unbounded Knapsack: Coin Change
Expand Code
1def coinChange(self, coins: List[int], amount: int) -> int: 2 def unbounded_knapsack(): 3 # f(i, j) = min number of coins from the set coins[0...i] to make up for amount j 4 # transition rule 5 # f(i, j) = min(f(i-1, j), f(i, j-coins[i]) + 1) 6 """ 7 if appears that we are only considering the case where we take coins[i] just once. 8 but when we PROCESS THIS LEFT TO RIGHT, so, if f(i, j-k*coins[i]) really was 9 the optimal ans for something we'd have considered it. 10 11 !!!!!IMPORTANT!!!!! 12 when the state (i,j) is updated, all (i,k<j) states should have been updated with 13 the knowledge that coins[i] exists in this world! 14 """ 15 # removing the first dimension 16 # f(j) = min(f(j), f(j-coins[i]) + 1) 17 n = len(coins) 18 dp = [inf] * (amount + 1) 19 dp[0] = 0 20 for coin in coins: 21 for j in range(coin, amount + 1): 22 dp[j] = min(dp[j], dp[j-coin] + 1) 23 return dp[-1] if dp[-1] < inf else -1 24 25 def bfs_bottom_up(): 26 # the idea is to start traversing on the state transition 27 # graph. start node is amount, and destination node is 0. 28 # for each node in this graph, we have coins outgoing edges 29 # O(V+E) = O(S+S*N) 30 queue = deque([0]) 31 visited = set([]) 32 steps = 0 33 while queue: 34 size = len(queue) 35 for _ in range(size): 36 curr_amount = queue.popleft() 37 if curr_amount == amount: 38 return steps 39 for coin in coins: 40 new_amount = curr_amount + coin 41 if new_amount <= amount and new_amount not in visited: 42 queue.append(new_amount) 43 visited.add(new_amount) 44 steps += 1 45 return -1 46 def bfs_top_down(): 47 # the idea is to start traversing on the state transition 48 # graph. start node is amount, and destination node is 0. 49 # for each node in this graph, we have coins outgoing edges 50 # O(V+E) = O(S+S*N) 51 queue = deque([amount]) 52 visited = set([]) 53 steps = 0 54 while queue: 55 size = len(queue) 56 for _ in range(size): 57 curr_amount = queue.popleft() 58 if curr_amount == 0: 59 return steps 60 for coin in coins: 61 remaining_amount = curr_amount - coin 62 if remaining_amount >= 0 and remaining_amount not in visited: 63 queue.append(remaining_amount) 64 visited.add(remaining_amount) 65 steps += 1 66 return -1 67 68def coinChangeCountWays(self, amount: int, coins: List[int]) -> int: 69 """ Note: f(i,j) = f(i-1,j) + f(i,j-coins[i]) """ 70 # f(i-1,j) := don't use j-th coin 71 # f(i,j-coins[i]) := use j-th coin 72 dp = [0] * (amount + 1) 73 for coin in coins: 74 dp[0] = 1 75 for j in range(coin, amount + 1): 76 dp[j] += dp[j-coin] 77 return dp[-1]
Expand Code
1def minEditDistance(self, word1: str, word2: str) -> int: 2 # f(i,j) = edit distance after seeing s[0...i-1] and t[0...j-1] 3 # f(i+1, j+1) = f(i, j) if s[i] == t[j] 4 # else 5 """ 6 Trick: 7 (1) Think of the next character we'd look after each operation. 8 (2) Normalize the equations afterwards 9 """ 10 # replace: f(i+1,j+1) = f(i,j) + 1 11 # delete: f(i+1,j) = f(i,j) + 1 => f(i+1,j+1) = f(i,j+1) + 1 12 # insert: f(i, j+1) = f(i,j) + 1 => f(i+1,j+1) = f(i+1,j) + 1 13 """ 14 Final equations: 15 f(i+1,j+1) = f(i,j) if s[i] == s[j] else (min(f(i,j),f(i,j+1),f(i+1,j))) + 1 16 17 Cannot be reduced to lower dimensions - need both (i,j) and (i+1,j) at the same time 18 """ 19 m, n = len(word1), len(word2) 20 dp = [[0] * (n+1) for _ in range(m+1)] 21 """ Key: IMPORTANT TO INITIALIZE PROPERLY """ 22 for i in range(m + 1): 23 dp[i][0] = i # all delete 24 for j in range(n + 1): 25 dp[0][j] = j # all insert 26 for i in range(m): 27 for j in range(n): 28 if word1[i] == word2[j]: 29 dp[i+1][j+1] = dp[i][j] 30 else: 31 dp[i+1][j+1] = min(dp[i][j+1], dp[i+1][j], dp[i][j]) + 1 32 return dp[-1][-1]
Expand Code
1def maximalSquare(self, matrix: List[List[str]]) -> int: 2 # if the current entry in the matrix is 1, then it can contribute 3 # to 3 possible squares, one ending with top (i-1, j), one on the left 4 # (i, j-1) and one ending on the corner (i-1, j-1) 5 # case by case explanation: 6 # if the square ending at (i-1, j) is the min, then we know for sure 7 # that the one ending at (i, j-1) covers at least as many rows as (i-1, j). 8 # this is guaranteed since they are SQUARES not RECTANGLES. 9 m, n = len(matrix), len(matrix[0]) if len(matrix) > 0 else 0 10 prev_row = [1 if x == '1' else 0 for x in matrix[0]] 11 curr_row = [0] * n 12 result = max(prev_row) 13 for i in range(1, m): 14 curr_row[0] = 1 if matrix[i][0] == '1' else 0 15 result = max(result, curr_row[0]) 16 for j in range(1, n): 17 if matrix[i][j] == '1': 18 curr_row[j] = min(curr_row[j-1], prev_row[j-1], prev_row[j]) + 1 19 result = max(result, curr_row[j]) 20 for j in range(n): 21 prev_row[j] = curr_row[j] 22 curr_row[j] = 0 23 return result * result 24 25def maximalRectangle(self, matrix: List[List[str]]) -> int: 26 # the approach for maximal square doesn't apply here 27 # we solve it by folding it and forming histograms 28 def max_consecutive_ones(arr): 29 left = 0 30 res = 0 31 for right in range(len(arr)): 32 if arr[right] == 0: 33 left = right + 1 34 else: 35 res = max(right - left + 1, res) 36 return res 37 38 def largest_histogram(heights): 39 # we scan from left to right and check the min of every interval 40 # everything that gets popped is the min of some interval 41 max_area = 0 42 minstack = [-1] 43 for i, height in enumerate(heights): 44 while minstack[-1] != -1 and heights[minstack[-1]] >= height: 45 prev_min_index = minstack.pop() 46 # prev_min_index is the min of the range(stack[-1] + 1, i - 1) 47 left, right = minstack[-1] + 1, i - 1 # IMPORTANT: THIS IS WHY WE NEED -1 48 width = right - left + 1 49 max_area = max(max_area, width * heights[prev_min_index]) 50 minstack.append(i) 51 # everything that remains on stack is also min of some range 52 while minstack[-1] != -1: 53 prev_min_index = minstack.pop() 54 # prev_min_index is the min of the range(stack[-1] + 1, n - 1) 55 left, right = minstack[-1] + 1, len(heights) - 1 56 width = right - left + 1 57 max_area = max(max_area, width * heights[prev_min_index]) 58 return max_area 59 60 m, n = len(matrix), len(matrix[0]) if len(matrix) > 0 else 0 61 if not m or not n: 62 return 0 63 64 histogram = [1 if x == '1' else 0 for x in matrix[0]] 65 max_area = max_consecutive_ones(histogram) 66 for i in range(1, m): 67 for j in range(n): 68 histogram[j] = histogram[j] + 1 if matrix[i][j] == '1' else 0 69 max_area = max(max_area, largest_histogram(histogram)) 70 return max_area
Expand Code
1def wordBreak(self, s: str, wordDict: List[str]) -> bool: 2 def good_approach(): 3 """ O(n*m*k) """ 4 # f(i) := s[0...i] can be segmented or not 5 # f(i) = true of any f(j-1, j <= i) and s[j,i] in in dict 6 words = set(wordDict) 7 n = len(s) 8 dp = [False] * n 9 10 for i in range(n): 11 for word in words: 12 size = len(word) 13 j = i-size+1 14 if (j == 0 or (j > 0 and dp[j-1])) and (j >= 0 and s[j:i+1] == word): 15 dp[i] = True 16 break 17 18 return dp[-1] 19 20 def bad_approach(): 21 """ O(n^2) """ 22 # f(i) := s[0...i] can be segmented or not 23 # f(i) = true of any f(j-1, j <= i) and s[j,i] in in dict 24 words = set(wordDict) 25 n = len(s) 26 dp = [False] * n 27 28 for i in range(n): 29 for j in range(i+1): 30 if (j == 0 or dp[j-1]) and s[j:i+1] in words: 31 dp[i] = True 32 break 33 34 return dp[-1] 35 36 return good_approach() 37 38def wordBreakII(self, s: str, wordDict: List[str]) -> List[str]: 39 def backtrack(curr, start_index): 40 nonlocal result 41 n = len(s) 42 if start_index >= n: 43 ans = ' '.join(curr) 44 result.append(ans) 45 return 46 47 for j in range(start_index, n): 48 if is_word(start_index, j): 49 size = j - start_index + 1 50 word = s[start_index: start_index + size] 51 curr.append(word) 52 backtrack(curr, start_index + size) 53 curr.pop() 54 55 def is_word(i, j): 56 nonlocal words 57 # curr = s[i:j+1] 58 # return curr in words 59 60 61 words = set(wordDict) 62 n = len(s) 63 # if dp[i][j] = True => s[i...j] is a valid word in the dict 64 dp = [[False] * n for _ in range(n)] 65 66 # TODO implement using trie 67 print(dp) 68 result = [] 69 backtrack([], 0) 70 return result
Greedy¶
Single CPU Scheduling (Unordered With Delay Restrictions)
Expand Code
1""" WHEN TASKS HAS TO BE EXECUTED IN ANY ORDER """ 2def taskScheduler(self, tasks: List[str], n: int) -> int: 3 def heap_queue(): 4 # greedy: 5 # since the order does not matter, pick the those teasks 6 # in the decreasing order of counts. 7 # 8 # IMPORTANT 9 # process tasks with one tick at a time 10 # to do that, we can maintain two data structures 11 # (1) for the tasks which can be processed right away 12 # (2) tasks which are in the queue to be available for processing later 13 counts = Counter(tasks) 14 # we don't need to reduce counts in the original dict 15 # we can totally maintain the states using the two data structures we have 16 # since we don't are about the type of the task, we don't need to keep 17 # the task name as well 18 # ready: priority queue -> counts 19 # queued: counts, start_time 20 ready = [-count for count in counts.values()] 21 heapq.heapify(ready) 22 queued = deque() 23 curr_time = 0 24 while ready or queued: 25 if queued and queued[0][1] < curr_time: 26 count, _ = queued.popleft() 27 heapq.heappush(ready, count) 28 if ready: 29 count = 1 + heapq.heappop(ready) 30 if count: 31 queued.append((count, curr_time + n)) 32 curr_time += 1 33 return curr_time 34 35 def idle_counting(): 36 # visualisation: 37 # 38 39 return heap_queue() 40 41""" WHEN TASKS HAS TO BE EXECUTED IN GIVEN ORDER """ 42def taskSchedulerII(self, tasks: List[int], space: int) -> int: 43 available_from = {} 44 curr_day = 0 45 for task in tasks: 46 if task in available_from: 47 """ IMPORTANT """ 48 # while doing timejump remember that we don't go back in past 49 curr_day = max(curr_day, available_from[task]) # time jump 50 curr_day += 1 # execute task 51 available_from[task] = curr_day + space 52 return curr_day
Resource Allocation
Expand Code
1def canAttendAllMeetings(self, intervals: List[List[int]]) -> bool: 2 def overlaps(first, second): 3 return first[1] > second[0] 4 5 n = len(intervals) 6 # sort the intervals by start time 7 # sorting helps because if any given interval (curr) 8 # doesn't overlaps with the immediate next interval 9 # then none of the next intervals would be overlapping with curr 10 intervals.sort(key=lambda x:x[0]) 11 for i in range(1, n): 12 if overlaps(intervals[i-1], intervals[i]): 13 return False 14 return True 15 16def maxTimeMeetingRoomBooked(self, intervals: List[List[int]]) -> List[List[int]]: 17 def overlaps(interval1, interval2): 18 # since the calling ensures that interval1 has earlier start time 19 # than interval2, we just need to check if interval1 ends after 20 # interval2 begins. 21 return interval1[1] >= interval2[0] 22 23 def merge(interval1, interval2): 24 # similar to above, the calling ensures that interval1 starts earlier 25 # than interval2. therefore, for merged interval, we only need to check 26 # the max of end times 27 return [interval1[0], max(interval1[1], interval2[1])] 28 29 intervals.sort() 30 stack = [intervals[0]] 31 32 for interval in intervals[1:]: 33 if overlaps(stack[-1], interval): 34 last = stack.pop() 35 stack.append(merge(last, interval)) 36 else: 37 stack.append(interval) 38 39 return stack 40 41def minMeetingsDropped(self, intervals: List[List[int]]) -> int: 42 # removing overlapping intervals is a different prolem 43 # than counting intervals. 44 # why? 45 # say, we have 3 intervals, i1, i2, i3 where i1 overlaps with i2 46 # and i2 overlaps with i3. if we count overlaps, there are 2. 47 # if we remove i1, overlap count reduces to 1 48 # if we remove i3, overlap count reduces to 1 49 # but if we remove i2, overlap count reduces to 0 50 # so this requires a different strategy 51 # greedy activity selection. basically the idea is to think of it 52 # as meetings which are to be held in a single room. we are the doorman 53 # and we want to maximize the room usage by assigning it to the most 54 # number of meetings possible. this can be achieved by assigning it to 55 # the meeting that ends first. since this choice doesn't hurt with the objective. 56 intervals.sort(key=lambda x:x[1]) # sorted by end time 57 n = len(intervals) 58 meetings_count = 1 59 last_meeting_finish_time = intervals[0][1] 60 for start_time, finish_time in intervals[1:]: 61 if last_meeting_finish_time <= start_time: 62 meetings_count += 1 63 last_meeting_finish_time = finish_time 64 return n - meetings_count 65 66def minMeetingRooms(self, intervals: List[List[int]]) -> int: 67 # it asks for minimum number of resources that is required 68 # to allocate all meetings. this means, every time there 69 # are overlapping meetings, we need to find a new room to 70 # accomodate for it. if intervals are represented as nodes in 71 # a graph, and we use edges to connect those nodes whenever they 72 # are overlapping, then this problem can be thought of as a 73 # graph colouring problem on that interval graph. 74 # the algorithm works by simulation of the situation. 75 # we need to maintain two lists - one for occupied rooms and one 76 # for free rooms that we've already decided to pay for. 77 # every time a new meeting comes, we first check if there are occupied 78 # rooms which can be marked as free. we add them to the free rooms list. 79 # we allocate room from this free list as we've already paid for them. 80 # if there really are no free rooms, we increase max room count, and decide 81 # to pay for one more extra room 82 intervals.sort() 83 occupied = [] # needs to store finish time 84 # instead of free list, can we manage using just the count? 85 free_rooms_count = 1 86 max_rooms_used = 1 87 88 for start_time, finish_time in intervals: 89 while occupied and occupied[0] <= start_time: 90 heapq.heappop(occupied) 91 free_rooms_count += 1 92 if free_rooms_count: 93 heapq.heappush(occupied, finish_time) 94 free_rooms_count -= 1 95 else: 96 max_rooms_used += 1 97 heapq.heappush(occupied, finish_time) 98 99 return max_rooms_used 100 101def mostBooked(self, n: int, meetings: List[List[int]]) -> int: 102 # here, the room id matters 103 # need to maintain not just free rooms, but the room ids as well 104 # lucky for us, our allocation rule indicates that we can pick 105 # rooms from the free list in order of their ids. 106 # so we maintain the same occupied and free lists. but we also 107 # store the room ids. 108 meetings.sort() 109 occupied = [] # stores finish time and room id 110 free = list(range(n)) # stores room id 111 112 # we also need to do bookkeeping for meetings counts 113 counts = defaultdict(int) 114 115 for start_time, end_time in meetings: 116 duration = end_time - start_time 117 118 while occupied and occupied[0][0] <= start_time: 119 _, room_id = heapq.heappop(occupied) 120 heapq.heappush(free, room_id) 121 122 if free: 123 # can allocate a room right now 124 room_id = heapq.heappop(free) 125 heapq.heappush(occupied, (end_time, room_id)) 126 counts[room_id] += 1 127 else: 128 # need to wait until another room becomes free 129 # need to adjust end time accordingly 130 finish_time, room_id = heapq.heappop(occupied) 131 heapq.heappush(occupied, (finish_time + duration, room_id)) 132 counts[room_id] += 1 133 134 max_meetings = max(counts.values()) 135 # we return the room that held max meetings and has the lowest index 136 # this can be achieved by traversing the key list in counts in sorted 137 # order. lucky for us, dict already has keys in sorted order 138 return [x for x in counts if counts[x] == max_meetings][0] 139 140def busiestServers(self, k: int, arrival: List[int], load: List[int]) -> List[int]: 141 # similar to meeting roms 3, here also we need to maintain 142 # two lists for free resources and occupied resources 143 # but unlike meeting rooms 3, where we were to wait until a room was free 144 # and our preference was to allocate rooms to the lowest id possible, 145 # here, we have a fixed allocation rule according to round robin. 146 # if there are no free rooms at the time of arrival, request is dropped. 147 # the round robin works on sorted room id sequences. 148 # example: 149 # at any given time, free rooms [0,1,5], and we have 8 resources [0-7] 150 # request 12 comes. 12 % 8 = 4 151 # if server 4 was available, we would have used it. but since it's not, 152 # the next best we can do is 5. note that this is the insert position of 4 153 # in the sorted free rooms list 154 # does this hold for everything? 155 # any incoming requests would be mapped to [0, 7] due to modulo division. 156 # say, request 16 comes. server index = 16 % 8 = 0 157 # since 0 is already there, we allocate it. 158 159 # arrivals are already sorted 160 occupied = [] # stores the finish time and room_id 161 free = SortedList(list(range(k))) # stores room ids in sorted order 162 # SortedList is a balanced BST 163 # we could have used SortedSet since rooms are unique 164 # but SortedList provides us with bisect_left which is what we need 165 166 # bookkeeping for counts 167 counts = defaultdict(int) 168 169 for i, start_time in enumerate(arrival): 170 end_time = start_time + load[i] 171 172 while occupied and occupied[0][0] <= start_time: 173 _, server_id = heapq.heappop(occupied) 174 free.add(server_id) 175 176 if free: 177 # key point to observe here is that as long as we 178 # have free servers, we can ALWAYS allocate resource 179 # in a round robin fashion 180 server_id = i % k 181 index = free.bisect_left(server_id) 182 # there is a server which can serve the request 183 server_id = free[index] if index < len(free) else free[0] 184 free.remove(server_id) 185 heapq.heappush(occupied, (end_time, server_id)) 186 counts[server_id] += 1 187 188 max_count = max(counts.values()) 189 # since keys in dict are in sorted order, this returns 190 # the list of server ids in sorted order 191 return [x for x in counts if counts[x] == max_count]