1D Dynamic Programming

Table Of Contents
1D Dynamic Programming Intro
- What is 1D Dynamic Programming
- Recursive Memoization: Improving Recursion
- Iterative: Tabulation and Variables
- (0 to i or i to N) Recursive Top Down vs (0 to i) Iterative Bottom Up
- DP Array Size / Padding / Indexing / Shifting
- 1D Dynamic Programming IRL
- 1D Dynamic Programming Application: DFS with Caching (Padded N+1) Top Down with Memoization
- 1D Dynamic Programming Application: DFS with Caching (Direct N) Top Down with Memoization
- 1D Dynamic Programming Application: Iterative Tabulation (Padded N+1) Bottom Up
- 1D Dynamic Programming Application: Iterative Tabulation (Direct N) Bottom Up
- 1D Dynamic Programming Application: Optimal Iterative (Padded N+1) Rolling State Variables Bottom Up
- 1D Dynamic Programming Application: Optimal Iterative (Direct N) Rolling State Variables Bottom Up
- 1D Dynamic Programming Application: Opposite Ends Iterative (Direct N) Rolling State Variables Bottom Up
- 1D Dynamic Programming Application: Sliding Window Iterative (Direct N) Rolling State Variables Bottom Up
- 1D Dynamic Programming Application: BFS Visited Memo Level Order
70. Climbing Stairs ::4:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Padded N+1) Top Down with Memoization
- Solution 2: 0 to i Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Padded N+1) Top Down with Memoization
- Solution 3: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Padded N+1) Bottom Up
- Solution 4: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Padded N+1) Rolling State Variables Bottom Up
746. Min Cost Climbing Stairs ::4:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Padded N+1) Top Down with Memoization
- Solution 2: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 3: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 4: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
1137. Nth Tribonacci Number ::3:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
198. House Robber ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
213. House Robber II ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
5. Longest Palindromic Substring ::3:: - Medium
- Intro
- Abstraction
- Find the Bug: Did not create Expanded String
- Find the Bug: Defined List instead of Slicing
- Find the Bug: Bad iteration leading to out of bounds on string expansion
- Find the Bug: Bad enumerate method:
- Solution 1: Manacher's Algorithm (iterate, mirror radius optimization, and expand) - Two Pointers/Algorithm
- Solution 2: Expand Around Center checking for Odd and Even palindromes (constant space) - Two Pointers/Algorithm
- Solution 3: Dynamic Programming - 1D Dynamic Programming/Linear Property Tracking
91. Decode Ways ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
322. Coin Change ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 3: BFS Memo Coin Number Level Order Search - 1D Dynamic Programming/BFS Visited Memo Level Order
152. Maximum Product Subarray ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: (Modified Kadane) i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: (Modified Kadane) 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
- Solution 3: (Modified Kadane) 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
- Solution 4: (Modified Kadane) Forward Backward Scan - 1D Dynamic Programming/Opposite Ends Iterative (Direct N) Rolling State Variables Bottom Up
139. Word Break ::4:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 2: Optimized i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
- Solution 3: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Sliding Window Iterative (Direct N) Rolling State Variables Bottom Up
- Solution 4: 0 to i Iterative Bottom Up Rolling Variables Sliding Window - 1D Dynamic Programming/Sequential Segment Choice Validation
300. Longest Increasing Subsequence ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: Dynamic Programming - 1D Dynamic Programming/Subsequence Optimization Constrained
- Solution 2: Dynamic Programming - 1D Dynamic Programming/Subsequence Optimization Constrained
- Solution 3: Binary Search - 1D Dynamic Programming/Subsequence Optimization Constrained
1D Dynamic Programming Intro
LeetCode problems solved with dynamic programming
What is 1D Dynamic Programming
Dynamic Programming (DP) is a technique for solving problems that can be broken into overlapping sub problems and have optimal substructure, meaning the optimal solution can be built from optimal solutions of sub problems.
Instead of solving the same subproblem repeatedly, DP stores solutions in a table via memorization or a bottom up array, to avoid redundant work.
Recursive Memoization: Improving Recursion
Many 1D Dynamic Programming problems naturally arise from recursion. In climbing stairs problem, a naive recursive solutions explores all paths:
dfs(i) = dfs(i+1) + dfs(i+2)
While this recursion works logically, it recomputes overlapping sub problems.
- dfs(i) calls dfs(i+1) and dfs(i+2).
- dfs(i+1) recomputes dfs(i+2), with its own dfs(i+1) -> dfs(i+1+1)
- dfs(i+2) is computed twice, as n grows, recursion trees grows to O(2n)
Memoization solves this by caching results of sub problems to avoid recomputation.
Iterative: Tabulation and Variables
Dynamic Programming can also be implemented iteratively via bottom up by computing solutions from small sub problems to larger ones.
- Tabulation Full Array
Here we store the solution of ever sub problem in an array dp[]. Each state depends on previous states, and we compute all of them sequentially from 0 -> N. Leads to an array of O(n)
- Optimal
Often, each state depends on a fixed number of previous states, usually the last 1 or 2. Here, we replace the tabulation array with variables, reducing the space form O(n) to O(1).
(0 to i or i to N) Recursive Top Down vs (0 to i) Iterative Bottom Up
We have two ways to solve dynamic programming problems. Recursive and iterative, and each have their own strategy for building up problems.
- Top Down: Recursive + Memoization We start from the target and recursively break up the problem into sub problems until we reach the base case.
Ex: fib(n) = fib(n-1) + fib(n-2)
by calling fib(n-1) and fib(n-2) recursively, retrieving the value and getting fib(n)
Here, we run into overlapping sub problems which we solve with memoization to avoid recomputation.
- Bottom Up: Iterative Array and Variables We start from the bottom and iteratively build solutions up for larger sub problems until we reach the nth case.
Ex: solve fib(1), fib(2), fib(3) .... until we get ... fib(n)
by iterating from 3 to n, with formula fib(n) = fib(n-1) + fib(n-2)
Visual Comparison:
Top-Down: n
/ \
n-1 n-2
/ \ ...
...
computed recursively
Bottom-Up: 0 1 2 3 ... n
computed iteratively
DP Array Size / Padding / Indexing / Shifting
The choice of a DP array size of (N) or (N+1) depends on whether we are padding the DP array and thus what the DP array will represent.
For a naive approach, we could just do the mental trick: If we do size (N) we need dp[n-1] as final answer. If we do size (N+1) we need dp[n] as final answer.
- Prefix View (N+1) / 1 indexed View
Here, we 'pad' the DP array with one extra state that does not correspond to the original array. This extra state is usually the empty case or base case.
This leads to a 1 indexed view where arr[0] corresponds to dp[1].
dp[i] then represents the answer for the first i elements: steps[0..i-1]
dp[0] = answer for [] elements (empty case) dp[n] = answer for first [0...n-1] elements (all of them)
dp size then must be dp[n+1]
- Element View (N) / 0 Indexed View
Here, we do not 'pad' the DP array and instead do direct matching with the original array.
This leads to a 0 indexed view where arr[i] corresponds to dp[i].
dp[i] then represents the answer for element i: houses[i]
dp[0] = answer for 1 element (first element) dp[n-1] = answer for last element
dp size then must be dp[n]
House Robber: (N) + (N+1)
(N) Natural Here, dp[i] represents maximum money that can be robbed from houses 0..i
def rob(self, nums: List[int]) -> int:
n = len(nums)
# Edge case: only 1 house
# Without this, accessing nums[1] would cause IndexError
if n == 1:
return nums[0]
# dp[i] = max money robbed from houses 0..i
# Rule (2): "element i itself"
dp = [0] * n # direct:
dp[0] = nums[0] # direct: max loop up to ith house
dp[1] = max(nums[0], nums[1]) # direct: max loop up to 2nd house
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[n-1]
(N+1) Shifted Here, dp[i] represents the max money robbed from the first i houses. This shifts the definition forward by 1 and naturally uses n+1 size.
def rob(nums: List[int]) -> int:
n = len(nums)
# dp[i] = max money robbed from the first i houses
# Rule (1): "the first i elements"
dp = [0] * (n+1) # padding:
dp[0] = 0 # padding: (empty case, max loot from no houses)
dp[1] = nums[0]
for i in range(2, n+1):
dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])
return dp[n]
Climbing stairs: (N+1) + (N)
(N+1) Natural Here, dp[i] represents the number of ways to climb to step i Since you need to compute dp[n], you need n+1 states.
def climbStairs(n: int) -> int:
# dp[i] = ways to reach step i
# Rule (1): "the first i elements"
dp = [0] * (n+1) # padding:
dp[0] = 1 # padding: (ground case, 1 way: do nothing)
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
(N) Shifted Here, dp[i] represents the number of ways to reach steps (i+1). So the DP array is only of length n (last index n-1 = ways to reach step n).
def climbStairs(n: int) -> int:
# Edge cases:
# Without this, setting dp[1] would cause IndexError
if n == 1:
return 1
# dp[i] = ways to reach step (i+1)
# Rule (2): "element i itself"
dp = [0] * n # direct
dp[0] = 1 # direct: (ways to reach step 1)
dp[1] = 2 # direct: (ways to reach step 2)
for i in range(2, n):
dp[i] = dp[i-1] + dp[i-2]
return dp[n-1]
1D Dynamic Programming IRL
Networking: Optimize packet processing sequences with limited buffers.
Resource Allocation: Task scheduling with constraints (eg, cannot pick consecutive tasks)
Finance: Maximize profit in trading tracking best profit up to day if
1D Dynamic Programming Application: DFS with Caching (Padded N+1) Top Down with Memoization
Pattern: Recursive DFS explores choices, memo stores results to avoid recomputation. Extra base 'padding' simplifies boundary checks. Use When: Problem naturally needs a “ground” or empty state. Recurrence: dfs(i) = dfs(i-1) + dfs(i-2)
Ex: Climbing Stairs (Top Down, Padded)
def climbStairs(n: int) -> int:
memo = {}
def dfs(i: int) -> int:
if i in memo:
return memo[i]
if i == 0: # padding: ground, 1 way to do nothing
return 1
if i == 1: # padded: first step
return 1
# recursive relation: sum of previous two steps
memo[i] = dfs(i-1) + dfs(i-2)
return memo[i]
return dfs(n)
1D Dynamic Programming Application: DFS with Caching (Direct N) Top Down with Memoization
Pattern: Recursive DFS explores choices, memo stores results to avoid recomputation. Direct mapping aligns base cases with the first elements. Use When: Problem naturally aligns 1:1 with input array elements. Recurrence: dfs(i) = dfs(i-1) + dfs(i-2)
Ex: Climbing Stairs (Top Down, Direct)
def climbStairs(n: int) -> int:
memo = {}
def dfs(i: int) -> int:
if i in memo:
return memo[i]
if i == 1: # direct: 1st element
return 1
if i == 2: # direct: 2nd element
return 2
# recursive relation: sum of previous two steps
memo[i] = dfs(i-1) + dfs(i-2)
return memo[i]
return dfs(n)
1D Dynamic Programming Application: Iterative Tabulation (Padded N+1) Bottom Up
Pattern: Iteratively fill a DP array with extra padding for base/empty case. Use When: Problem naturally needs a “ground” or empty state and empty state was not given in original array. Recurrence: dp[i] = dp[i-1] + dp[i-2] (or problem-specific relation).
Ex: Climbing Stairs (Bottom Up, Padded)
def climbStairs(n: int):
# no length == 1 check, since using pad + 1st element
# 1st element will always exist, pad is made up
dp = [0] * (n + 1) # padding:
dp[0] = 1 # padding: (ground, 1 way to do nothing),
# does not exist in original array
dp[1] = 1 # padded: first element, n=0 -> dp[1]
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
1D Dynamic Programming Application: Iterative Tabulation (Direct N) Bottom Up
Pattern: Iteratively fill a DP array with direct mapping to the original array elements. Use When: Problem naturally aligns 1:1 with input array. Recurrence: dp[i] = max(dp[i-1], dp[i-2] + nums[i])
Ex: House Robber (Bottom Up, Direct)
def rob(nums):
n = len(nums)
# length == 1 check needed since using 1st + 2nd element
# 2nd element may not exist
if n == 1:
return nums[0]
dp = [0] * n # direct:
dp[0] = nums[0] # direct: 1st element
dp[1] = max(nums[0], nums[1]) # direct: 2nd element
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[n-1]
1D Dynamic Programming Application: Optimal Iterative (Padded N+1) Rolling State Variables Bottom Up
Pattern: Reduce DP array to a few variables while padding for base/ground case. Use When: Problem naturally needs a “ground” or empty state and you want O(1) space. Recurrence: curr = prev1 + prev2 -> shift forward each step.
Ex: Climbing Stairs (Optimal Bottom Up, Padded)
def climbStairs(n: int) -> int:
# no length == 1 check, since using pad + 1st element
# 1st element will always exist, pad is made up
# padded variables
prev2 = 1 # padding: ground (0 steps, 1 way to do nothing)
prev1 = 1 # first step
# no length check needed since prev1 and prev2 already cover n=1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2, prev1 = prev1, curr
return prev1
1D Dynamic Programming Application: Optimal Iterative (Direct N) Rolling State Variables Bottom Up
Pattern: Reduce DP array to a few variables with direct mapping to the original array. Use When: Problem naturally aligns 1:1 with input array and O(1) space is desired. Recurrence: curr = prev1 + prev2 → shift forward each step.
Ex: House Robber (Optimal Bottom Up, Direct)
def rob(nums) -> int:
n = len(nums)
# length == 1 check needed since using 1st + 2nd element
# 2nd element may not exist
if n == 1:
return nums[0]
prev2 = nums[0] # direct: first element
prev1 = max(nums[0], nums[1]) # direct: second element
for i in range(2, n):
curr = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, curr
return prev1
1D Dynamic Programming Application: Opposite Ends Iterative (Direct N) Rolling State Variables Bottom Up
Pattern: Maintain rolling cumulative products from both ends of the array to capture subarrays affected by negative numbers. Use When: Problem involves products/subarrays where negative numbers can flip the max/min, and O(1) space is desired. Recurrence: left_prod *= nums[i], right_prod *= nums[n-1-i], reset to 1 if zero.
Ex: Maximum Product Subarray (Opposite Ends Rolling Variables)
def maxProduct(nums: List[int]) -> int:
n = len(nums)
# Direct Variables -> rolling left and right products
left_prod = right_prod = 1
res = float('-inf')
for i in range(n):
# Build From Previous -> roll from left and right ends
left_prod *= nums[i]
right_prod *= nums[n-1-i]
# Three Possibilities -> compare overall max
res = max(res, left_prod, right_prod)
# Direct Boundary -> reset rolling on zero
if left_prod == 0:
left_prod = 1
if right_prod == 0:
right_prod = 1
return res
1D Dynamic Programming Application: Sliding Window Iterative (Direct N) Rolling State Variables Bottom Up
Pattern: Maintain a rolling DP window of fixed size instead of the full DP array to reduce space. Use When: Problem involves checking previous k states (e.g., word lengths in Word Break) and you want O(k) space instead of O(n). Recurrence: dp[i % (k+1)] = any(dp[(i-l) % (k+1)] and segment_valid for l in 1..k) od *= nums[n-1-i], reset to 1 if zero.
Ex: Word Break (Sliding Window Rolling Variables)
def wordBreak(s: str, wordDict: List[str]) -> bool:
n = len(s)
# Direct Length -> empty string check
if n == 0:
return True
word_set = set(wordDict)
max_len = max(map(len, wordDict)) if wordDict else 0
# Direct Variables -> initialize rolling DP window
dp = [False] * (max_len + 1)
dp[0] = True # base case: empty string
# Iterate -> 1 to n
for i in range(1, n + 1):
dp[i % (max_len + 1)] = False
# Build From Previous -> look back up to max_len positions
for l in range(1, min(i, max_len) + 1):
# Three Possibilities -> valid segmentation from previous l positions
if dp[(i - l) % (max_len + 1)] and s[i - l:i] in word_set:
dp[i % (max_len + 1)] = True
break # early stop, valid segmentation found
# Result -> final DP state
return dp[n % (max_len + 1)]
1D Dynamic Programming Application: BFS Visited Memo Level Order
Pattern: Treat problem as shortest-path search; each state represents “remaining target” or “current subproblem.” Use a queue for BFS and a visited set as memoization to avoid recomputation. Use When: Problem can be framed as reaching a target with choices at each step (e.g., coin change, min steps). BFS guarantees the first solution found is minimal (shortest path). Recurrence: For each state curr, explore all valid next states curr - choice. Steps to reach next = steps[curr] + 1.
Ex: Coin Change (BFS with Memo)
def coinChange(coins, amount):
if amount == 0:
return 0
# Queue stores (remaining amount, coins used so far)
queue = deque([(amount, 0)])
visited = set([amount]) # memo: avoid revisiting same remaining amount
while queue:
rem, steps = queue.popleft()
# Explore choices
for coin in coins:
next_rem = rem - coin
# Early stop: minimal coins found
if next_rem == 0:
return steps + 1
# Valid state & not visited
if next_rem > 0 and next_rem not in visited:
visited.add(next_rem)
queue.append((next_rem, steps + 1))
# Impossible to reach target
return -1
70. Climbing Stairs ::4:: - Easy
Topics: Math, Dynamic Programming, Memoization
Intro
You are climbing a staircase. It takes n steps to reach
the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
Example Input | Output |
---|---|
n = 2 | 2 |
n = 3 | 3 |
Constraints:
1 ≤ n ≤ 45
Abstraction
Given a number of steps, return the number of unique ways to reach the top, by either climbing 1 or 2 steps at a time.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Padded N+1) Top Down with Memoization
def climbStairs(self, n: int) -> int:
# Note:
# Top Down DP (memoized recursion)
# 0. Direct Length -> no need for len == n check, recursive padding boundary covers it
# 1. Memo Check -> computed previously
# 2. Pad Boundaries -> overshot case case
# 3. Padded Boundary -> last element n
# 3. Build From Previous -> grab from previous two steps and sum paths
# 5. Memo Return -> return ways, calculated once
# Result: Ways to climb from 0 to nth step
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Pad Boundaries -> overshot case case
if i > n:
return 0
# Padded Boundary -> last element n
if i == n:
return 1
# Build From Previous -> grab from previous two steps and sum paths
memo[i] = dfs(i + 1) + dfs(i + 2)
# Memo return: return ways, calculated once
return memo[i]
# res -> ways to get from 0 to nth step
res = dfs(0)
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursion stack)
return res
Solution 2: 0 to i Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Padded N+1) Top Down with Memoization
def climbStairs(self, n: int) -> int:
# Note:
# Top Down DP (memoized recursion)
# 0. Direct Length -> no need for len == 1 check, recursive padding boundary covers it
# 1. Memo Check -> computed previously
# 2. Pad Boundaries -> 0 ground case
# 3. Padded Boundary -> 1st element
# 4. Build From Previous -> grab from previous two steps and sum paths
# 5. Memo return -> return ways, calculated once
# Result: Ways to climb from 0 to nth step
# Ways to climb from 0 to ith step
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Pad Boundaries -> 0 ground case
if i == 0:
return 1
# Padded Boundary -> 1st element
if i == 1:
return 1
# Build From Previous -> grab from previous two steps and sum paths
memo[i] = dfs(i - 1) + dfs(i - 2)
# Memo return -> return ways, calculated once
return memo[i]
# res -> ways to climb from 0 to nth step
res = dfs(n)
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursion stack)
return res
Solution 3: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Padded N+1) Bottom Up
def climbStairs(self, n: int) -> int:
# Note:
# Iterative Bottom Up Tabulation
# 1. Pad Array -> 0 ground case, dp size (N+1)
# 2. Padded -> grab 1st element
# 3. Iterate -> 2 to n
# 4. Build From Previous -> grab from previous two steps and sum paths
# Result: ways to climb to nth step
# Pad Array -> 0 ground case, dp size (N+1)
dp = [0] * (n + 1) # padding:
dp[0] = 1 # padding: ground case, does not exist in array
dp[1] = 1 # padded: first step
# Iterate -> 2 to nth
for i in range(2, n+1):
# Build From Previous -> grab from previous two steps and sum paths
dp[i] = dp[i-1] + dp[i-2]
# res -> nth step
res = dp[n]
# overall: time complexity O(n)
# overall: space complexity O(n)
return res
Solution 4: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Padded N+1) Rolling State Variables Bottom Up
def climbStairs(self, n: int) -> int:
# Note:
# Iterative Bottom Up Variables
# 1. Pad Variables -> 0 ground case
# 2. Padded -> grab 2st element
# 3. Iterate -> 2 to n
# 4. Build From Previous -> grab from previous two steps and sum paths
# 5. Roll Variables -> Iterate Variables
# Result: ways to climb to nth step
# Pad Variables -> 0 ground case
TwoPrev = 1 # padding:
OnePrev = 1 # padded: 1st element
# Iterate -> 2 to n
for i in range(2, n+1):
# Build From Previous -> grab from previous two steps and sum paths
curr = TwoPrev + OnePrev
# Slide Window -> Roll Variables
TwoPrev, OnePrev = OnePrev, curr
# res -> ways to climb to n step
res = OnePrev
# overall: time complexity O(n)
# overall: space complexity O(1)
return res
746. Min Cost Climbing Stairs ::4:: - Easy
Topics: Array, Dynamic Programming
Intro
You are given an integer array cost where cost[i] is the cost of ith step on a staircase. Once you pay the cost, you can either climb one or two steps. You can either start from the step with index 0, or the step with index 1. Return the minimum cost to reach the top of the floor.
Example Input | Output |
---|---|
cost = [10,15,20] | 15 |
[1,100,1,1,1,100,1,1,100,1] | 6 |
Constraints:
2 ≤ cost.length ≤ 1000
0 ≤ cost[i] ≤ 999
Abstraction
Given a number of steps, and the cost to process a step, find the cheapest cost to climb to the top.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Padded N+1) Top Down with Memoization
def minCostClimbingStairs(self, cost: List[int]) -> int:
# Note:
# Top Down DP (memoized recursion)
# 0. Direct Length -> no need for len == 1 check, recursive padding boundary covers it
# 1. Memo Check -> computed previously
# 2. Padding Boundary -> ground step 0
# 3. Padded Boundary -> first element
# 4. Build From Previous -> grab from previous two steps and sum paths
# 5. Memo return -> return ways, calculated once
# Result: min cost to reach n from last or 2nd to last step
n = len(cost)
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Padding Boundary -> ground step 0
if i == 0:
return 0
# Padded Boundary -> first element
if i == 1:
return cost[0]
# Build From Previous -> min cost to reach current step
memo[i] = cost[i-1] + min(dfs(i-1), dfs(i-2))
return memo[i]
# res -> reach step n by coming from last two steps
res = min(dfs(n), dfs(n-1))
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursive stack)
return res
Solution 2: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def minCostClimbingStairs(self, cost: List[int]) -> int:
# Note:
# Top Down DP (memoized recursion)
# 0. Direct Length -> no need for len == 1 check, recursive direct boundary covers it
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> 1st element
# 3. Direct Boundary -> 2nd element
# 4. Direct Build From Previous -> grab from previous two steps and sum paths
# 5. Memo return -> return ways, calculated once
# Result: min cost to reach n from last or 2nd to last step
n = len(cost)
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Direct Boundary -> 1st element
if i == 0:
return cost[0]
# Direct Boundary -> 2nd element
if i == 1:
return cost[1]
# Build -> min cost to reach + cost to continue
memo[i] = cost[i] + min(dfs(i-1), dfs(i-2))
return memo[i]
# res -> reach n by continuing from nth or nth-1 step (since we step 1 or 2 steps)
res = min(dfs(n-1), dfs(n-2))
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursive stack)
return res
Solution 3: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def minCostClimbingStairs(self, cost: List[int]) -> int:
# Note:
# Bottom Up Tabulation
# 0. Direct Length -> no need for len == 1 check, description says min length == 2
# 1. Direct Array -> 1st + 2nd element
# 3. Iterate -> 2 to n-1
# 4. Build From Previous -> grab from previous two steps and sum paths
# Result: min cost to reach n via continuing from step n-1 or n-2
# 0. Direct Length -> no need for len == 1 check, description says min length == 2
n = len(cost)
# Direct Array -> 1st + 2nd element
dp = [0] * n
dp[0], dp[1] = cost[0], cost[1]
# Iterate -> steps 2 to n-1
for i in range(2, n):
# Build From Previous -> grab from previous two steps and sum paths
dp[i] = cost[i] + min(dp[i-1], dp[i-2])
# res -> min cost to reach n
res = min(dp[n-1], dp[n-2])
# overall: time complexity O(n)
# overall: space complexity O(n)
return res
Solution 4: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
def minCostClimbingStairs(self, cost: List[int]) -> int:
# Note:
# Bottom Up Variables
# 0. Direct Length -> no need for len == 1 check, description says min length == 2
# 1. Direct Variables -> 1st + 2nd element
# 2. Iterate -> 2 to n-1
# 3. Build From Previous -> grab from previous two steps and sum paths
# 3. Roll Variables -> Iterate Variables
# Result: min cost to reach n via continuing from last or 2nd to last step (n-1, or n-2)
# 0. Direct Length -> no need for len == 1 check, description says min length == 2
n = len(cost)
# Direct Variables -> 1st + 2nd element
prev2, prev1 = cost[0], cost[1]
# Iterate -> 2 to n-1
for i in range(2, n):
# Build From Previous -> grab from previous two steps and sum paths
curr = cost[i] + min(prev1, prev2)
# Roll Variables -> Iterate Variables
prev2, prev1 = prev1, curr
# res -> min cost to reach n
res = min(prev1, prev2)
# overall: time complexity O(n)
# overall: space complexity O(1)
return res
1137. Nth Tribonacci Number ::3:: - Easy
Topics: Math, Dynamic Programming, Memoization
Intro
The Tribonacci sequence Tn is defined as follows: T0 = 0, T1 = 1, T2 = 1, Tn+3 = Tn + Tn+1 + Tn+2 for n >= 0. Given n, return the value of Tn.
Example Input | Output |
---|---|
n = 4 | 4 |
n = 25 | 1389537 |
Constraints:
0 ≤ n ≤ 37
The answer is guaranteed to fit within a 32-bit integer, ie. answer ≤ 231 - 1
Abstraction
Find the Tribonacci for n.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def tribonacci(self, n: int) -> int:
# Note:
# Top Down DP (memoized recursion)
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> 1st element
# 3. Direct Boundary -> 2nd element
# 4. Direct Boundary -> 3rd element
# 5. Build From Previous -> grab from previous two steps and sum paths
# 6. Memo return -> return ways, calculated once
# Result: T_i in Tribonacci sequence
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Boundary -> 1st element
if i == 0:
return 0
# Boundary -> 2nd element
if i == 1:
return 1
# Boundary -> 3rd element
if i == 2:
return 1
# Build From Previous -> grab from previous two steps and sum paths
memo[i] = dfs(i-1) + dfs(i-2) + dfs(i-3)
return memo[i]
# res -> T_n value
res = dfs(n)
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursion stack)
return res
Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def tribonacci(n: int) -> int:
# Note:
# this is a unique case where Direct/N, requires an array of (N+1)
# since we have 0 -> N fib solutions
# Note:
# Iterative Bottom Up Tabulation
# 1. Direct Boundary -> need for len == 0, len == 1, and len == 2, as 3rd element may not exist
# 2. Direct Array -> 1st, 2nd, and 3rd element
# 3. Iterate -> 2 to n-1
# 4. Iterate -> 3 to n
# 5. Build From Previous -> sum previous three DP entries
# Result: T_n in Tribonacci sequence
# Direct Boundary -> len == 0, len == 1, and len == 2, as 3rd element may not exist
if n == 0:
return 0
if n == 1:
return 1
# Direct Array -> 1st, 2nd, and 3rd element
dp = [0] * (n+1)
dp[0], dp[1], dp[2] = 0, 1, 1
# Iterate -> 3 to n
for i in range(3, n+1):
# Build From Previous -> sum of last three
dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
# res -> nth tribonacci
res = dp[n]
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursive stack)
return res
Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
def tribonacci(self, n: int) -> int:
# Note:
# this is a unique case where Direct/N, requires an array of (N+1)
# since we have 0 -> N fib solutions
# Note:
# Iterative Bottom Up Variables
# 1. Direct Boundary -> need len == 1 check, elements 2 and 3 may not exist
# 2. Direct Variables -> 1st + 2nd + 3rd elements
# 3. Iterate -> 3 to n
# 4. Build From Previous -> grab from previous two steps and sum paths
# 5. Roll Variables -> Iterate Variables
# Result: T_n in Tribonacci sequence
# Direct Boundary -> need len == 1 check, elements 2 and 3 may not exist
if n == 0:
return 0
if n == 1:
return 1
# Direct Variables -> 1st + 2nd + 3rd elements
ThreePrev, TwoPrev, OnePrev = 0, 1, 1
# Iterate -> 3 to n
for i in range(3, n+1):
# Build From Previous -> grab from previous two steps and sum paths
curr = ThreePrev + TwoPrev + OnePrev
# Roll Variables -> Iterate Variables
ThreePrev, TwoPrev, OnePrev = TwoPrev, OnePrev, curr
# res -> nth tribonacci
res = OnePrev
# overall: time complexity O(n)
# overall: space complexity O(1)
return res
198. House Robber ::3:: - Medium
Topics: Array, Dynamic Programming
Intro
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night. Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Example Input | Output |
---|---|
nums = [1,2,3,1] | 4 |
nums = [2,7,9,3,1] | 12 |
Constraints:
1 ≤ nums.length ≤ 100
0 ≤ nums[i] ≤ 400
Abstraction
Given an array of cash, determine the max you can steal when you cannot steal from two adjacent entries.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Explicit Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def rob(self, nums: List[int]) -> int:
# Note:
# Top down recursive with memoization
# 0. Direct Length -> no need for len == 1 check, recursive padding boundary covers it
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> 1st element
# 3. Direct Boundary -> 2nd element
# 3. Build From Previous -> grab from previous two steps and sum paths
# Result: max loop from first to last house
n = len(nums)
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Direct Boundary -> 1st element
if i == 0:
return nums[0]
# Direct Boundary -> 2nd element
if i == 1:
return max(nums[0], nums[1])
# Direct Build From Previous -> grab from previous two steps and sum paths
# choose max between rob or skipping current house
memo[i] = max(nums[i] + dfs(i-2), dfs(i-1))
return memo[i]
# res -> max loot for nth house
res = dfs(n-1)
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursion stack)
return res
Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def rob(self, nums: List[int]) -> int:
# Note:
# Bottom Up Tabulation
# 0. Direct Length -> need for len == 1 check, 2nd element may not exist
# 1. Direct Array -> 1st + 2nd element
# 2. Iterate -> 2 to n-1
# 3. Build From Previous -> grab from previous two steps and sum paths
# Result: max loot from nth house
n = len(nums)
# Direct Length -> need for len == 1 check, 2nd element may not exist
if n == 1:
return nums[0]
# Direct Array -> 1st + 2nd element
dp = [0] * n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
# Iterate -> 2 to n-1
for i in range(2, n):
# Build From Previous -> grab from previous two steps and sum paths
# choose max between rob or skipping current house
dp[i] = max(nums[i] + dp[i-2], dp[i-1])
# res -> max loot at nth house
res = dp[n-1]
# overall: time complexity O(n)
# overall: space complexity O(n)
return res
Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
def rob(self, nums: List[int]) -> int:
# Note:
# Bottom Up Variables
# 0. Direct Length -> need for len == 1 check, 2nd element may not exist
# 1. Direct Variables -> 1st + 2nd element
# 2. Iterate -> 2 to n-1
# 3. Build From Previous -> grab from previous two steps and sum paths
# 4. Roll Variables -> Iterate Variables
# Result: max loot first to last house
n = len(nums)
# Direct Length -> need for len == 1 check, 2nd element may not exist
if n == 1:
return nums[0]
# Direct Variables -> 1st + 2nd element
prev2, prev1 = nums[0], max(nums[0], nums[1])
# Iterate -> 2 to n-1
for i in range(2, n):
# Build From Previous -> grab from previous two steps and sum paths
curr = max(prev1, nums[i] + prev2)
# Roll Variables -> Iterate Variables
prev2, prev1 = prev1, curr
# res -> max loot at nth house
res = prev1
# overall: time complexity O(n)
# overall: space complexity O(1)
return res
213. House Robber II ::3:: - Medium
Topics: Array, Dynamic Programming
Intro
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night. Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Example Input | Output |
---|---|
nums = nums = [2,3,2] | 3 |
nums = [1,2,3,1] | 4 |
nums = [1,2,3] | 3 |
Constraints:
1 ≤ nums.length ≤ 100
0 ≤ nums[i] ≤ 1000
Abstraction
Given an array of cash, determine the max you can steal when you cannot steal from two adjacent entries, when array is circular.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def rob(self, nums: List[int]) -> int:
# Note:
# Top down (recursive with memoization)
# 0. Direct Length -> need for len == 1 check, 2nd element may not exist
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> 1st + 2nd element
# 3. Build From Previous -> grab from previous two steps and sum paths
# 4. Handle circular constraint -> cannot rob both first and last
# Result: max loot for circular house array
n = len(nums)
# Direct Length -> need for len == 1 check, 2nd element may not exist
if n == 1:
return nums[0]
def rob_range(start, end):
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Direct Boundary -> i == 0 (in subarray)
if i == start:
return nums[i]
# Direct Boundary -> i == 1 (in subarray)
if i == start + 1:
return max(nums[i], nums[i - 1])
# Build From Previous -> grab from previous two steps and sum paths
memo[i] = max(nums[i] + dfs(i-2), dfs(i-1))
return memo[i]
# res -> max loot at last house in subarray
return dfs(end)
# Handle circular constraint -> cannot rob both first and last
res1 = rob_range(0, n-2)
res2 = rob_range(1, n-1)
# res -> max loot from circular houses
res = max(res1, res2)
# overall: time complexity O(n)
# overall: space complexity O(n) (memo + recursion stack)
return res
Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def rob(self, nums: List[int]) -> int:
# Note:
# Bottom up array
# 0. Direct Length -> need for len == 1 check, 2nd element may not exist
# 1. Direct Length (subarray) -> need for len == 1 check, 2nd element may not exist
# 2. Direct Array -> 1st + 2nd element
# 3. Iterate -> 2 to numHouses-1
# 4. Build From Previous -> grab from previous two steps and sum paths
# 5. Handle circular constraint -> 2 subsets to break circular array
# Result: max loot from circular houses array
n = len(nums)
# Direct Length -> need for len == 1 check, 2nd element may not exist
if n == 1:
return nums[0]
def rob_range(start, end):
# Direct Length -> need for len == 1 check, 2nd element may not exist
m = end - start + 1
if m == 1:
return nums[start]
# Direct Array -> 1st + 2nd element
dp = [0] * m
dp[0] = nums[start]
dp[1] = max(nums[start], nums[start + 1])
# Iterate -> 2 to m-1
for i in range(2, m):
j = start + i
# Build From Previous -> grab from previous two steps and sum paths
# choose max between rob or skipping current house
dp[i] = max(nums[j] + dp[i-2], dp[i-1])
# res -> max loot at last house in subarray
return dp[m-1]
# Handle circular constraint -> cannot rob both first and last
res1 = rob_range(0, n-2)
res2 = rob_range(1, n-1)
# res -> max loot from circular houses
res = max(res1, res2)
# overall: time complexity O(n)
# overall: space complexity O(n)
return res
Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
def rob(self, nums: List[int]) -> int:
# Note:
# Bottom up variables
# 0. Direct Length -> need for len == 1 check, 2nd element may not exist
# 1. Direct Length (subarray) -> need for len == 1 check, 2nd element may not exist
# 2. Direct Variables -> 1st + 2nd element
# 3. Iterate -> 2 to numHouses-1
# 4. Build From Previous -> grab from previous two steps and sum paths
# 5. Roll Variables -> Iterate Variables
# 6. Handle circular constraint -> cannot rob both first and last
# Result: max loot from circular houses
n = len(nums)
# Direct Length -> need for len == 1 check, 2nd element may not exist
if n == 1:
return nums[0]
def rob_range(start, end):
# Direct Length -> need for len == 1 check, 2nd element may not exist
m = end - start + 1
if m == 1:
return nums[start]
# Direct Variables -> 1st + 2nd element
TwoPrev = nums[start]
OnePrev = max(nums[start], nums[start+1])
# Iterate -> 2 to m-1
for i in range(2, m):
j = start + i
# Build From Previous -> grab from previous two steps and sum paths
curr = max(nums[j] + TwoPrev, OnePrev)
# Roll Variables -> Iterate Variables
TwoPrev, OnePrev = OnePrev, curr
# res -> max loot at last house in subarray
res = OnePrev
return res
# Handle circular constraint -> cannot rob both first and last
res1 = rob_range(0, n-2)
res2 = rob_range(1, n-1)
# res -> max loot from circular houses
res = max(res1, res2)
# overall: time complexity O(n)
# overall: space complexity O(1)
return res
5. Longest Palindromic Substring ::3:: - Medium
Topics: Two Pointers, String, Dynamic Programming
Intro
Given a string s, return the longest palindromic substring in s.
Input | Output |
---|---|
"cbbd" | "bb" |
"babad" | "bab" or "aba" |
Constraints:
1 ≤ s.length ≤ 1000
s consists of only digits and English letters.
Abstraction
Find the longest palindrome in a string.
Find the Bug: Did not create Expanded String
def longestPalindrome(self, s: str) -> str:
# INCORRECT:
# did not create expandedStr
# did not solve odd vs even palindrome problem
# missing:
# expandedStr = "#".join(f"^{s}$")
center = 0
right = 0
n = len(s)
p = [0] * n
for i in range(1, n-1):
# 1
mirror = (2*center) - i
# 2
if i < right:
p[i] = min(right-i, p[mirror])
# 3
while s[i + p[i] + 1] == s[i - p[i] - 1]:
p[i] += 1
# 4
if i + p[i] > right:
center = i
right = i + p[i]
(maxRadius, center) = max((maxRadius, i) for (i, maxRadius) in enumerate(p))
start = (center-maxRadius) // 2
return s[start:start+maxRadius]
Find the Bug: Defined List instead of Slicing
def longestPalindrome(self, s: str) -> str:
def expandAroundCenter(left, right):
while left >= 0 and right < n and s[left] == s[right]:
left -= 1
right += 1
# INCORRECT:
# defined list instead of slicing
# should be: s[left+1:right]
return s[left+1, right]
n = len(s)
maxPalin = ""
for i in range(n):
oddPalin = expandAroundCenter(i, i)
evenPalin = expandAroundCenter(i, i+1)
if len(oddPalin) > len(maxPalin):
maxPalin = oddPalin
if len(evenPalin) > len(maxPalin):
maxPalin = evenPalin
return maxPalin
Find the Bug: Bad iteration leading to out of bounds on string expansion
def longestPalindrome(self, s: str) -> str:
expandedStr = "#".join(f"^{s}$")
n = len(expandedStr)
p = [0] * n
center = 0
right = 0
# INCORRECT:
# iteration will also expand from the sentinals '^' and '$'
for i in range(n):
# grab mirror
mirror = (2*center)-i
# validate mirror
if i < right:
p[i] = min(p[mirror], (right-i))
# expand
# INCORRECT:
# due to iteration, expansion is not guaranteed and prevent
# out of bounds grab, since we are missing the eventual
# '^' != '$'
while expandedStr[i - p[i] - 1] == expandedStr[i + p[i] + 1]:
p[i] += 1
# find new right most
if p[i] + i > right:
center = i
right = p[i] + i
# translate back to height
maxRadi, center = max((maxRadi, center) for (center, maxRadi) in enumerate(p))
startIndex = (center-maxRadi)//2
return s[startIndex:startIndex+maxRadi]
Find the Bug: Bad enumerate method:
def longestPalindrome(self, s: str) -> str:
expandedStr = "#".join(f"^{s}$")
n = len(expandedStr)
p = [0] * n
center = 0
right = 0
for i in range(1, n-1):
# grab mirror
mirror = (2*center)-i
# validate mirror
if i < right:
p[i] = min(p[mirror], (right-i))
# expand
while expandedStr[i - p[i] - 1] == expandedStr[i + p[i] + 1]:
p[i] += 1
# find new right most
if p[i] + i > right:
center = i
right = p[i] + i
# translate back to height
# INCORRECT:
# should be
# enumerate(p)
# instead of p.enumerate()
maxRadi, center = max((maxRadi, center) for (center, maxRadi) in p.enumerate())
startIndex = (center-maxRadi)//2
return s[startIndex:startIndex+maxRadi]
Solution 1: Manacher's Algorithm (iterate, mirror radius optimization, and expand) - Two Pointers/Algorithm
def longestPalindrome(self, s: str) -> str:
# Note:
# Preprocessing with #, ^, and $:
# '#': ensures uniform expansion, for both odd and even length palindromes
# '^' and '$': sentinel characters don't match valid input characters, serve as true start and end markers
# '#': ensures all palindromes start and end with '#'
# '#': occur on odd indexes
# Mapping:
# we can map odd '#' indexes to their even character:
# mapping '#' at index 1 to char pair 'a' at index 2, to original 'a' at index 0
# [ ^ # a # b # a # $ ] -> [ a b a ] via : originalStart = (expandedCenter - radius) / 2
# 0 1 2 3 4 5 6 7 8 0 1 2 thus: originalStart = (4 - 3) / 2 = 0
# Boundary expansion:
# For any index i, center of palindrome at i can either be:
# - character from the original string
# - placeholder '#'
# Center definition allows even length palindromes such as "abba", see below,
# to have a single middle point, allowing the same expanding logic
# for even and odd strings for palindrome validation
# Ex:
# ^ # a # b # b # a # $ || new string len 11,
# 0 1 2 3 4 5 6 7 8 9 10 ||
# ^ || index 5 center for even length "abba"
# index 1 palindrome: "#"
# index 2 palindrome: "#a#"
# index 5 palindrome: "#a#b#b#a#"
# etc...
expandedStr = "#".join(f"^{s}$")
n = len(expandedStr)
# Right Most Palindrome and Mirror Trick:
# Iteration tracks the right boundary for the current farthest right palindromic substring,
# which allows us to take advantage of the mirror radius trick.
# It speeds up palindrome expansion by starting the current palindrome radius
# at the radius value of its mirror
# p[i]: radius of palindrome centered at some index i
p = [0] * n
# mirror radius validation: tracking right boundary
# right: right boundary of the current right most palindrome
right = 0
# mirror radius validation: tracking center of right most in order to calculate mirror index
# center: center index of the current right most palindrome
center = 0
# iteration range ignores sentinel indexes 0 and (n-1): ^ and $
# i: center of current palindrome
# time complexity: iterate over list of n length O(n)
for i in range(1, n - 1):
# mirror:
# i is current index being processed
# i is to the right of center and has a mirror to the left of center
# ex: center = 6, i = 7 => mirror = (2 * 6) - 7 = 5
mirror = (2 * center) - i
# mirror radius validation:
# if i lies within bounds of the right most palindrome,
# the right most palindrome symmetry guarantees that the palindrome radius
# for the mirror of i, is applicable to i as well,
# while within the bounds of the right most palindrome
if i < right:
# mirror radius is either:
# - less than the distance between i and right bound,
# in which case all of the radius is valid
# - exceeds bounds and is farther than right bound,
# in which case only the radius up until the right bound is valid
# i radius is thus, bounded by minimum between:
# - mirror radius
# - distance from i to the right bound
p[i] = min(right - i, p[mirror])
# assumption: if valid mirror, we pre-set p[i] to p[mirror]
# now expand: expand radius p[i] until palindromic status is broken
while expandedStr[i + p[i] + 1] == expandedStr[i - p[i] - 1]:
p[i] += 1
# p[i]: radius for palindrome at i
# i: center for palindrome at i
# check: if we have a new right most boundary, update center and right
if i + p[i] > right:
right = i + p[i]
center = i
# expandedStr iteration complete:
# p[] stores radius of palindrome centered at each index
# scan p[] grabbing max palindrome radius alongside its center
maxRadius, centerIndex = max((palindromeRadius, i) for (i, palindromeRadius) in enumerate(p))
# Note:
# index and radius are relative to expandedStr, not the original string
# thus, we need to translate to original string indexes
# Notice, how in the expanded string,
# - all original characters are on even index
# - all original characters have matching # on the left odd index
# abba => ^ # a # b # b # a # $ | a=2, b=4, b=6, a=8
# 0123 => 0 1 2 3 4 5 6 7 8 9 10 | #=1, #=3, #=5, #=7
# aba => ^ # a # b # a # $ | a=2, b=4, a=6
# 012 => 0 1 2 3 4 5 6 7 8 | #=1, #=3, #=5
# any palindrome will always end with a '#'.
# so if we divide the starting odd position by 2, it will always map
# to an original character.
# so an easy translation formula is:
start = (centerIndex - maxRadius) // 2
# splice longest substring
# overall: time complexity O(n)
# overall: space complexity O(n)
return s[start: start + maxRadius]
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Preprocessing | O(n) | O(n) | Building expanded string | Memory allocation for processed string O(n) |
Iterating | O(n) | O(1) | Iterate over processed string of n length O(n) | No additional memory allocation for iteration O(1) |
Expanding radii | O(n) | O(n) | Radius expansion over processed string of n length O(n) | Radius array to store radii for each index for string of n length O(n) |
Updating mirror bounds | O(1) | O(1) | Updating center and right for right most palindrome in constant O(1) | No additional memory allocation for center and right variables O(1) |
Overall | O(n) | O(n) | Iterating over expanded string dominates, leading to O(n) time complexity. | Expanding string dominates, leading to O(n) space complexity. |
Solution 2: Expand Around Center checking for Odd and Even palindromes (constant space) - Two Pointers/Algorithm
def longestPalindrome(self, s: str) -> str:
# expand from a given left and right while
# maintaining palindrome property
def expandAroundCenter(left, right):
while left >= 0 and right < n and s[left] == s[right]:
left -= 1
right += 1
# curr iteration is not valid:
# ignore left: incrementing index
# ignore right: noninclusive right slicing
return s[left+1:right]
n = len(s)
maxPalindrome = ""
# time complexity: iterate over list of n length O(n)
for i in range(n):
# odd expansion, centered at i
oddPalindrome = expandAroundCenter(i, i)
# even expansion, centered at i and i + 1
evenPalindrome = expandAroundCenter(i, i+1)
# update longest
if len(oddPalindrome) > len(maxPalindrome):
maxPalindrome = oddPalindrome
if len(evenPalindrome) > len(maxPalindrome):
maxPalindrome = evenPalindrome
# overall: time complexity O(n^2)
# overall: space complexity O(1)
return maxPalindrome
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Iterating | O(n) | O(1) | Iterating over list of n length O(n) | No additional memory allocation for iteration O(1) |
Odd expansion | O(n^2) | O(1) | For each center, expands outward for odd length palindromes n length O(n), for each outer iteration O(n), leading to O(n^2) | No additional memory allocation needed during expansion O(1) |
Even expansion | O(n^2) | O(1) | For each center, expands outward for even length palindromes n length O(n), for each outer iteration O(n), leading to O(n^2) | No additional memory allocation needed during expansion O(1) |
Updating longest | O(1) | O(1) | Comparing odd and even length palindrome to current max in constant O(1) | No additional memory allocation needed for comparison to current max O(1) |
Overall | O(n^2) | O(1) | Even and odd expansion per outer iteration dominate, leading to O(n2) | No additional memory allocation required for in place expansion or iteration, leading to constant O(1) |
Solution 3: Dynamic Programming - 1D Dynamic Programming/Linear Property Tracking
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False]*n for _ in range(n)]
start, max_len = 0, 1
for i in range(n):
dp[i][i] = True
for length in range(2, n+1): # substring length
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
if length == 2 or dp[i+1][j-1]:
dp[i][j] = True
if length > max_len:
start, max_len = i, length
return s[start:start+max_len]
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
647. Palindromic Substrings ::2:: - Medium
Topics: Two Pointers, String, Dynamic Programming
Intro
Given a string s, return the number of palindromic substrings in it. A string is a palindrome when it reads the same backward as forward. A substring is a contiguous sequence of characters within the string.
Example Input | Output |
---|---|
s = "abc" | 3 |
s = "aaa" | 6 |
Constraints:
1 ≤ s.length ≤ 1000
s consists of lowercase English letters.
Abstraction
Given a string, determine how many palindromic substrings exist.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Two Pointers Expand Around Center - 1D Dynamic Programming/Linear Property Tracking
def countSubstrings(self, s: str) -> int:
n = len(s)
count = 0
# helper: expand from center
def expand(l: int, r: int) -> int:
local_count = 0
while l >= 0 and r < n and s[l] == s[r]:
local_count += 1 # found a palindrome
l -= 1
r += 1
return local_count
# expand around all possible centers
for i in range(n):
count += expand(i, i) # odd-length palindromes
count += expand(i, i + 1) # even-length palindromes
return count
Solution 2: Dynamic Programming - 1D Dynamic Programming/Linear Property Tracking
def countSubstrings(self, s: str) -> int:
n = len(s)
dp = [[False] * n for _ in range(n)]
count = 0
# single characters
for i in range(n):
dp[i][i] = True
count += 1
# substring lengths 2 -> n
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
if length == 2 or dp[i + 1][j - 1]:
dp[i][j] = True
count += 1
return count
91. Decode Ways ::3:: - Medium
Topics: String, Dynamic Programming
Intro
You have intercepted a secret message encoded as a string of numbers. The message is decoded via the following mapping: "1" -> 'A' "2" -> 'B' ... "25" -> 'Y' "26" -> 'Z' However, while decoding the message, you realize that there are many different ways you can decode the message because some codes are contained in other codes ("2" and "5" vs "25"). For example, "11106" can be decoded into: "AAJF" with the grouping (1, 1, 10, 6) "KJF" with the grouping (11, 10, 6) The grouping (1, 11, 06) is invalid because "06" is not a valid code (only "6" is valid). Note: there may be strings that are impossible to decode. Given a string s containing only digits, return the number of ways to decode it. If the entire string cannot be decoded in any valid way, return 0. The test cases are generated so that the answer fits in a 32-bit integer.
Example Input | Output |
---|---|
s = "12" | 2 |
s = "226" | 3 |
s = "06" | 0 |
Constraints:
1 ≤ s.length ≤ 100
s contains only digits and may contain leading zero(s).
Abstraction
Given a string, determine how many ways there to decode the string.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def numDecodings(self, s: str) -> int:
# Note:
# Top Down DP (recursive with memoization)
# 0. Direct Length -> no need for len == 1 check, recursive padding boundary covers it
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> 1st element: empty string
# 3. Direct Boundary -> 2nd element: first char
# 4. Build From Previous -> grab from previous two steps and sum paths
# Result: total decode ways from start (0) to end (n-1)
n = len(s)
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Direct Boundary -> 1st element: empty string
if i < 0:
return 1
# Direct Boundary -> 2nd element: first char
if i == 0:
return 1 if s[0] != '0' else 0
count = 0
# Build From Previous -> grab from previous two steps and sum paths
# 'How many total ways can I decode up to here if I decide to treat
# the current group as a valid single or valid double digit?'
# Add corresponding counts from single: i-1 and double: i-2
# check previous char, if single is valid
if s[i] != '0':
count += dfs(i-1)
# check double via previous char, if double is valid
two_digit = int(s[i-1:i+1])
if 10 <= two_digit <= 26:
count += dfs(i-2)
memo[i] = count
return memo[i]
# res -> number of decode ways for nth
res = dfs(n-1)
# overall: time complexity
# overall: space complexity
return res
Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def numDecodings(self, s: str) -> int:
# Note:
# Iteration here is unique here because we cant initialize/skip i = 1,
# because it introduces the first valid two digit choice.
# If we jumped directly to i = 2, we’d miss the case where the answer
# depends entirely on that first double digit decode.
# Note:
# Bottom Up DP
# 0. Direct Length -> need for len == 1 check, 2nd element/first char may not exist
# 1. Direct Array -> 1st element
# 2. Iterate -> 1 to n-1
# 3. Build From Previous -> grab from previous two steps and sum paths
# Result: total decode ways nth
n = len(s)
# Direct Length -> need for len == 0 check, 2nd element/first char may not exist
if not s:
return 0
# Direct Array -> 1st element
dp = [0] * n
dp[0] = 1 if s[0] != '0' else 0
# Iterate -> 1 to n-1
for i in range(1, n):
# Build From Previous -> grab from previous two steps and sum paths
# 'How many total ways can I decode up to here if I decide to treat
# the current group as a valid single or valid double digit?'
# Add corresponding counts from single: i-1 and double: i-2
# check previous char, if single is valid
if s[i] != '0':
dp[i] += dp[i-1]
# check double via previous char, if double is valid
two_digit = int(s[i-1:i+1])
if 10 <= two_digit <= 26:
dp[i] += dp[i-2] if i >= 2 else 1
# res -> number of decode ways for nth
res = dp[n-1]
# overall: time complexity O(n)
# overall: space complexity O(n)
return res
Solution 3: 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
def numDecodings(self, s: str) -> int:
# Note:
# Iteration here is unique here because we cant initialize/skip i = 1,
# because it introduces the first valid two digit choice.
# If we jumped directly to i = 2, we’d miss the case where the answer
# depends entirely on that first double digit decode.
# Note:
# Bottom-up DP with space optimization
# 0. Direct Length -> need for len == 0 check, 2nd element/first char, maybe not exist
# 1. Direct Variables -> 1st element
# 2. Iterate -> 1 to n-1
# 3. Build From Previous -> grab from previous two steps and sum paths
# 4. Roll Variables -> Iterate Variables
# Result -> total decode ways nth
# Direct Length -> need for len == 0 check, 2nd element/first char, maybe not exist
if not s:
return 0
n = len(s)
# Direct Variables -> 1st element
prev2 = 0
prev1 = 1 if s[0] != '0' else 0
# Iterate -> 1 to n-1
for i in range(1, n):
curr = 0
# Build From Previous -> grab from previous two steps and sum paths
# 'How many total ways can I decode up to here if I decide to treat
# the current group as a valid single or valid double digit?'
# Add corresponding counts from single: i-1 and double: i-2
# check previous char, if single is valid
if s[i] != '0':
curr += prev1
# decode ignoring double digit
two_digit = int(s[i-1:i+1])
if 10 <= two_digit <= 26:
curr += prev2 if i >= 2 else 1
# Roll Variables -> Iterate Variables
prev2, prev1 = prev1, curr
# res -> number of decode ways for nth
res = prev1
# overall: time complexity O(n)
# overall: space complexity O(1)
return res
322. Coin Change ::3:: - Medium
Topics: Array, Dynamic Programming, Breadth First Search
Intro
You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1. You may assume that you have an infinite number of each kind of coin.
Example Input | Output |
---|---|
coins = [1,2,5], amount = 11 | 3 |
coins = [2], amount = 3 | -1 |
coins = [1], amount = 0 | 0 |
Constraints:
1 ≤ coins.length ≤ 12
1 ≤ coins[i] ≤ 231 - 1
0 ≤ amount ≤ 104
Abstraction
Given a array of coin cost and a price, determine the minimum number of coins needed to make the price.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def coinChange(self, coins: List[int], amount: int) -> int:
# Note:
# Top down (recursive with memoization)
# 0. Direct Length -> no need for len == 1 check, recursive padding boundary covers it
# 1. Memo Check -> computed previously
# 2. Boundary Check -> exact match
# 3. Build From Previous -> grab from previous two steps and sum paths
# 4. Early Prune -> check if current coin leads to possible, else skip
# Result: min coins to reach amount if possible
sentinel = float('inf')
memo = {}
def dfs(curr_amount) -> int:
# Memo Check -> computed previously
if curr_amount in memo:
return memo[curr_amount]
# Boundary Check -> exact match
if curr_amount == 0:
return 0
curr_min = sentinel
# Build From Previous -> grab from previous two steps and sum paths
for coin in coins:
# Early Prune -> check if current coin leads to possible, else skip
new_remaining = curr_amount - coin
if new_remaining >= 0:
# check if new min coin beats previous
use_coin = dfs(new_remaining)
curr_min = min(curr_min, use_coin+1)
# res -> min coins to reach current amount
memo[curr_amount] = curr_min
return memo[curr_amount]
# Result -> remaining amount set to full, grab min number of coins
res = dfs(amount)
# overall: time complexity
# overall: space complexity
return -1 if res == float('inf') else res
Solution 2: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def coinChange(self, coins: List[int], amount: int) -> int:
# Note:
# Bottom Up Array
# 0. Direct Length -> amount=0 check, needs no coins
# 1. Direct Array -> dp[0] = 0
# 3. Iterate -> 1 to amount
# 4. Explore Choices -> try each coin (coin multiple times possible)
# 5. Build -> min between
# Result: min coins to reach amount if possible
sentinel = float('inf')
# Direct Array -> 1st element
dp = [0] * (amount + 1)
dp[0] = 0
# Iterate -> 1 to amount
for amount in range(1, amount + 1):
curr_min = sentinel
# Build From Previous -> grab from previous two steps and sum paths
for coin in coins:
# Early Prune -> check if current coin leads to possible, else skip
new_remaining = amount - coin
if new_remaining >= 0:
# check if new min coin beats previous
use_coin = dp[new_remaining] + 1
curr_min = min(curr_min, use_coin)
dp[amount] = curr_min
#
res = dp[amount] if dp[amount] != sentinel else -1
# overall: time complexity
# overall: space complexity
return res
Solution 3: BFS Memo Coin Number Level Order Search - 1D Dynamic Programming/BFS Visited Memo Level Order
def coinChange(self, coins: List[int], amount: int) -> int:
# Note:
# BFS Level Order (shortest path) to find min coins
# 0. Direct Length -> if amount is 0, no coins needed
# 1. Iterative stack -> (remaining_amount, coins_used_so_far)
# 2. Visited set -> no revisiting amount
# 1. Process root -> current state (amount, coins)
# 2. Explore choices -> subtract each coin to get next remaining amounts
# 3. Build -> increment steps (coins used) at each BFS level
# 4. Early stop -> return steps+1 when remaining reaches 0
# 5. Result -> -1 if queue exhausted (impossible)
# Direct Length -> if amount is 0, no coins needed
if amount == 0:
return 0
# Iterative stack -> (remaining_amount, coins_used_so_far)
queue = deque([(amount, 0)])
# memo_visited set -> no revisiting amount
memo_visited = set([amount])
while queue:
# Process Root -> current state (amount, coins)
rem, steps = queue.popleft()
# Build -> increment steps (coins used) at each BFS level
for coin in coins:
next_rem = rem - coin
# Early stop -> found a valid combination
if next_rem == 0:
return steps + 1
# Build -> enqueue next remaining if valid and not visited
elif next_rem > 0 and next_rem not in visited:
visited.add(next_rem)
queue.append((next_rem, steps + 1))
# No combination found
return -1
152. Maximum Product Subarray ::2:: - Medium
Topics: Array, Dynamic Programming
Intro
Given an integer array nums, find a subarray that has the largest product, and return the product. The test cases are generated so that the answer will fit in a 32-bit integer.
Example Input | Output |
---|---|
nums = [2,3,-2,4] | 6 |
nums = [-2,0,-1] | 0 |
Constraints:
1 ≤ nums.length ≤ 2 * 104
-10 ≤ nums[i] ≤ 10
The product of any subarray of nums is guaranteed to fit in a 32-bit integer.
Abstraction
Given a array, find the subarray with the largest product and return the product.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: (Modified Kadane) i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def maxProduct(self, nums: List[int]) -> int:
# Note:
# Top Down (recursive with memoization)
# 0. Direct Length -> empty check
# 1. Memo Check -> computed previously
# 1. Direct Boundary -> 1st element
# 2. Build From Previous -> grab previous max/min and calculate new max/min at i
# 3. Three Possibilities -> infer max subarray
# Result -> track overall maximum from all dfs calls
n = len(nums)
# Direct Length -> empty array check
if n == 0:
return 0
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Direct Boundary -> 1st element
if i == 0:
memo[0] = (nums[0], nums[0])
return memo[0]
# Build From Previous -> grab previous max/min and calculate new max/min at i
prev_max, prev_min = dfs(i-1)
# three possibilities
# 1. start new subarray at i -> num
# 2. extend previous max subarray -> num * prev_max
# 3. extend previous min subarray -> num * prev_min
num = nums[i]
curr_max = max(num, num * prev_max, num * prev_min)
curr_min = min(num, num * prev_max, num * prev_min)
# res -> max decision/subarray up to index i
memo[i] = (curr_max, curr_min)
return memo[i]
# start with max subarray at 0
result = nums[0]
for i in range(n):
# max subarray at i
curr_max, curr_min = dfs(i)
# res -> overall max subarray
result = max(result, curr_max)
# overall: time complexity O(n)
# overall: space complexity O(n)
return result
Solution 2: (Modified Kadane) 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Iterative Tabulation (Direct N) Bottom Up
def maxProduct(self, nums: List[int]) -> int:
# Note:
# Bottom Up DP with arrays
# 0. Direct Length -> empty array check
# 1. Direct Array -> initialize max_dp, min_dp at index 0
# 2. Iterate -> 1 to n-1
# 3. Build From Previous -> grab previous max/min and calculate new max/min at i
# 4. Three Possibilities -> infer max subarray
# Result -> grab overall max subarray
n = len(nums)
# Direct Length -> empty array check
if n == 0:
return 0
# Direct Array -> initialize max_dp, min_dp at index 0
max_dp = [0] * n
min_dp = [0] * n
max_dp[0] = min_dp[0] = result = nums[0]
# Iterate -> 1 to n-1
for i in range(1, n):
# three possibilities
# 1. start new subarray at i -> num
# 2. extend previous max subarray -> num * prev_max
# 3. extend previous min subarray -> num * prev_min
num = nums[i]
max_dp[i] = max(num, max_dp[i-1] * num, min_dp[i-1] * num)
min_dp[i] = min(num, max_dp[i-1] * num, min_dp[i-1] * num)
# res -> overall max subarray
result = max(result, max_dp[i])
# overall: time complexity O(n)
# overall: space complexity O(n)
return result
Solution 3: (Modified Kadane) 0 to i Iterative Bottom Up Variables - 1D Dynamic Programming/Optimal Iterative (Direct N) Rolling State Variables Bottom Up
def maxProduct(self, nums: List[int]) -> int:
# Note:
# Bottom Up DP (rolling variables)
# 0. Direct Length -> empty array check
# 1. Direct Variables -> initialize max_dp, min_dp at index 0
# 2. Iterate -> 1 to n-1
# 3. Build From Previous -> grab previous max/min and calculate new max/min at i
# 4. Three Possibilities -> infer max subarray
# Result -> grab overall max subarray
n = len(nums)
# Direct Length -> empty array check
if n == 0:
return 0
# Direct Variables -> initialize max_dp, min_dp at index 0
max_prod = min_prod = result = nums[0]
# Iterate -> 1 to n-1
for i in range(1, n):
# Store previous rolling values
prev_max, prev_min = max_prod, min_prod
# Build From Previous -> grab previous max/min and calculate new max/min at i
num = nums[i]
max_prod = max(num, prev_max * num, prev_min * num)
min_prod = min(num, prev_max * num, prev_min * num)
# Update overall result
result = max(result, max_prod)
# overall: time complexity O(n)
# overall: space complexity O(1)
return result
Solution 4: (Modified Kadane) Forward Backward Scan - 1D Dynamic Programming/Opposite Ends Iterative (Direct N) Rolling State Variables Bottom Up
def maxProduct(self, nums: List[int]) -> int:
# Note:
# Forward-Backward Scan (cumulative products)
# 0. Direct Length -> empty array check
# 1. Direct Variables -> initialize left and right
# 2. Iterate -> forward and backward in the same loop
# 4. Build -> update result with max(left_prod, right_prod)
# 5. Three Possibilities -> max subarray
# Result -> grab overall max subarray
n = len(nums)
# Direct Length -> empty array check
if n == 0:
return 0
# Direct Variables -> initialize left and right
left_prod = right_prod = 1
result = float('-inf')
# Iterate -> forward and backward in the same loop
for i in range(n):
# left scan -> contiguous subarray ending here
# right scan -> contiguous subarray starting from here
# new subarray after zero -> start of new subarray after zero
left_prod *= nums[i]
right_prod *= nums[n - 1 - i]
# Three Possibilities -> max subarray
result = max(result, left_prod, right_prod)
# Reset if zero encountered
if left_prod == 0:
left_prod = 1
if right_prod == 0:
right_prod = 1
# overall: time complexity O(n)
# overall: space complexity O(1)
return result
139. Word Break ::4:: - Medium
Topics: Array, Hash Table, String, Dynamic Programming, Trie, Memoization
Intro
Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space separated sequence of one or more dictionary words. Note that the same word in the dictionary may be reused multiple times in the segmentation.
Example Input | Output |
---|---|
s = "leetcode", wordDict = ["leet","code"] | true |
s = "applepenapple", wordDict = ["apple","pen"] | true |
s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] | false |
Constraints:
1 ≤ s.length ≤ 300
1 ≤ wordDict.length ≤ 1000
1 ≤ wordDict[i].length ≤ 20
s and wordDict[i] consist of only lowercase English letters.
All the strings of wordDict are unique.
Abstraction
Given a string and an array of strings, determine if the string can be separated into some combination of strings from the array.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# Note:
# Top Down DP (recursive with memoization)
# 0. Direct Length -> empty string boundary covered by recursion
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> empty string is valid segmentation
# 2. Iterate -> 0 to i, try every partition ending at i
# 3. Build From Previous -> if s[j:i] in dict and dfs(j) is True
# 4. Backtrack -> store False if no valid segmentation
# Result: can full string be segmented
n = len(s)
# int -> bool
memo = {}
# lookup
word_set = set(wordDict)
def dfs(i) -> bool:
# Direct Boundary -> empty string is valid segmentation
if i == 0:
return True
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Iterate -> 0 to i, try every partition ending at i
for j in range(i):
# Build From Previous -> valid segmentation
# check if s[j:i] is valid dictionary word
# checks if remaining substring before segment (start to j) can be segmented
if s[j:i] in word_set and dfs(j):
memo[i] = True
return True
# Backtrack -> no valid segmentation
memo[i] = False
return False
# res -> can segment full string
res = dfs(n)
# overall: time complexity O(n^2)
# overall: space complexity O(n)
return res
Solution 2: Optimized i to N Recursive with Memoization Top Down - 1D Dynamic Programming/DFS with Caching (Direct N) Top Down with Memoization
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# Note:
# Optimized Top Down DP (recursive with memoization)
# 0. Direct Length -> empty string boundary covered by recursion
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> empty string is valid segmentation
# 2. Explore Choices -> iterate only over words in wordDict
# 3. Build From Previous -> if s[i-len(word):i] matches and dfs(i-len(word)) is True
# 4. Backtrack -> store False if no valid segmentation
# Result: can full string be segmented
n = len(s)
memo = {}
# lookup
word_set = set(wordDict)
def dfs(i) -> bool:
# Direct Boundary -> empty string is valid segmentation
if i == 0:
return True
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Iterate -> try every word in dictionary
for word in word_set:
# Build From Previous -> valid segmentation
# i >= len(word) -> avoid nonsense negative slice: i = 3 -> substring = "app" -> s[i - len(word):i] == s[-2:3] == "pp"
# s[] -> check if word matches
if i >= len(word) and s[i - len(word):i] == word:
# check if left side after word is segmentable
if dfs(i - len(word)):
memo[i] = True
return True
# Backtrack -> no valid segmentation
memo[i] = False
return False
# res -> can segment full string
res = dfs(n)
# overall: time complexity O(n * m) (m = #words)
# overall: space complexity O(n)
return res
Solution 3: 0 to i Iterative Bottom Up Array - 1D Dynamic Programming/Sliding Window Iterative (Direct N) Rolling State Variables Bottom Up
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# Note:
# Bottom Up DP (tabulation)
# 0. Direct Length -> need len == 0 check
# 1. Direct Array -> empty string is segmentable
# 2. Iterate -> 1 to n
# 3. Explore Choices -> try all j < i
# 4. Build From Previous -> dp[i] = True if dp[j] is True and s[j:i] in dict
# 5. Early Prune -> break if segmentation found
# Result: can full string be segmented
n = len(s)
dp = [False] * (n + 1)
# quick lookup
word_set = set(wordDict)
# Direct Array -> empty string is segmentable
dp[0] = True
# Iterate -> 1 to n
for i in range(1, n+1):
# Explore Choices -> try all j < i
for j in range(i):
# Build From Previous -> dp[i] = True if dp[j] is True and s[j:i] in dict
if dp[j] and s[j:i] in word_set:
# Build -> segmentable
dp[i] = True
# early prune, found valid segmentation
break
# Result: can full string be segmented
res = dp[n]
# overall: time complexity
# overall: space complexity
return res
Solution 4: 0 to i Iterative Bottom Up Rolling Variables Sliding Window - 1D Dynamic Programming/Sequential Segment Choice Validation
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# Note:
# Bottom Up DP with rolling variables (sliding window)
# 0. Direct Length -> empty string check
# 1. Window Size -> track last max_word_length states only
# 2. Initialize -> dp[0] = True for empty string
# 3. Iterate -> 1 to n
# 4. Build From Previous -> check previous positions within window
# 5. Early stop -> break if valid segmentation found
# Result -> dp[n % (max_len + 1)] gives final result
n = len(s)
if n == 0:
return True
word_set = set(wordDict)
max_len = max(map(len, wordDict)) if wordDict else 0
# Initialize DP rolling window
dp = [False] * (max_len + 1)
dp[0] = True # empty string
# Iterate over string positions
for i in range(1, n + 1):
dp[i % (max_len + 1)] = False
# Build From Previous -> look back up to max_len
for l in range(1, min(i, max_len) + 1):
if dp[(i - l) % (max_len + 1)] and s[i - l:i] in word_set:
dp[i % (max_len + 1)] = True
break # early stop, found valid segmentation
# Result -> can full string be segmented
res = dp[n % (max_len + 1)]
# overall: time complexity
# overall: space complexity
return res
300. Longest Increasing Subsequence ::3:: - Medium
Topics: Array, Binary Search, Dynamic Programming
Intro
Given an integer array nums, return the length of the longest strictly increasing subsequence. Follow up: Can you come up with an algorithm that runs in O(n log(n)) time complexity?
Example Input | Output |
---|---|
nums = [10,9,2,5,3,7,101,18] | 4 |
nums = [0,1,0,3,2,3] | 4 |
nums = [7,7,7,7,7,7,7] | 1 |
Constraints:
1 ≤ nums.length ≤ 2500
-104 ≤ nums[i] ≤ 104
Abstraction
Given an integer array, find the longest increasing subsequence.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Dynamic Programming - 1D Dynamic Programming/Subsequence Optimization Constrained
def lengthOfLIS(self, nums: List[int]) -> int:
# Note:
# Top Down DP (recursive with memoization)
# 0. Direct Length -> empty array boundary covered by recursion
# 1. Memo Check -> computed previously
# 2. Direct Boundary -> single element subsequence has length 1
# 3. Explore Choices -> check all j > i to extend subsequence
# 4. Build From Previous -> max length using future extensions
# Result -> maximum LIS starting from index 0 to n-1
n = len(nums)
memo = {}
def dfs(i):
# Memo Check -> computed previously
if i in memo:
return memo[i]
# Direct Boundary -> single element
max_len = 1
# Explore Choices -> try to extend from i to future indices
for j in range(i + 1, n):
if nums[j] > nums[i]:
max_len = max(max_len, 1 + dfs(j))
memo[i] = max_len
return max_len
# Result -> try starting from every index
res = max(dfs(i) for i in range(n))
# overall: time complexity
# overall: space complexity
return res
Solution 2: Dynamic Programming - 1D Dynamic Programming/Subsequence Optimization Constrained
def lengthOfLIS(self, nums: List[int]) -> int:
# Note:
# Top Down DP (recursive memoization)
# 0. Direct Length -> empty array boundary covered by recursion
# 1. Direct array
# 2.
# 2. Direct Boundary -> single element subsequence has length 1
# 3. Explore Choices -> check all j > i to extend subsequence
# 4. Build From Previous -> max length using future extensions
# Result -> maximum LIS starting from index 0 to n-1
n = len(nums)
# Direct Length -> empty array boundary covered by recursion
if n == 0:
return 0
# dp[i] = length of LIS ending at i
dp = [1] * n
for i in range(n):
for j in range(i):
# Explore Choice -> extend subsequence ending at j
if nums[i] > nums[j]:
# Build -> max between current and extending j
dp[i] = max(dp[i], dp[j] + 1)
# res -> max from array
res = max(dp)
# overall: time complexity O(n^2)
# overall: space complexity. O(n) (memo + recursion stack)
return res
Solution 3: Binary Search - 1D Dynamic Programming/Subsequence Optimization Constrained
def lengthOfLIS(self, nums: List[int]) -> int:
# Note:
# Same as Solution 2, but manual binary search instead of bisect
# 1. Process root -> current number
# 2. Explore choices -> search for first tail >= num
# 3. Build -> append if end, replace otherwise
# 4. Result -> length of tails = LIS length
tails = []
#
for num in nums:
left, right = 0, len(tails) - 1
# Explore Choices -> find insertion/replacement index
while left <= right:
#
mid = (left + right) // 2
#
if tails[mid] < num:
left = mid + 1
else:
right = mid - 1
# Build: insert or replace
if left == len(tails):
tails.append(num)
else:
tails[left] = num
return len(tails)
416. Partition Equal Subset Sum ::2:: - Medium
Topics: Array, Dynamic Programming
Intro
Given an integer array nums, return true if you can partition the array into two subsets such that the sum of the elements in both subsets is equal or false otherwise.
Example Input | Output |
---|---|
nums = [1,5,11,5] | true |
nums = [1,2,3,5] | false |
Constraints:
1 ≤ nums.length ≤ 200
1 ≤ nums[i] ≤ 100
Abstraction
Given an integer array, determine if you can split it into two arrays such that the sum of each arrays is equal.
Space & Time Complexity
Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
---|---|---|---|---|
Bug | Error |
---|---|
Brute Force:
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Find the Bug:
Solution 1: Dynamic Programming Subset Sum - 1D Dynamic Programming/Subset Sum Linear Choice Selection
def canPartition(self, nums: List[int]) -> bool:
# Note:
# 1D DP for subset sum
# 1. Process root -> current number in nums
# 2. Explore choices -> include current number or skip it
# 3. Build -> update achievable sums in dp
# 4. Result -> check if target sum is achievable
total = sum(nums)
# If total sum is odd, cannot partition equally
if total % 2 != 0:
return False
target = total // 2
n = len(nums)
# dp[i] = True if sum i is achievable with some subset
dp = [False] * (target + 1)
# sum 0 is always achievable (root/base case)
dp[0] = True
for num in nums:
# Explore choices in reverse to avoid using same number twice
for i in range(target, num - 1, -1):
# Build: include num if sum i-num was achievable
dp[i] = dp[i] or dp[i - num]
# Result: can we achieve target sum?
return dp[target]
Solution 2: Bitmask Bitset DP - 1D Dynamic Programming/Subset Sum Linear Choice Selection
def canPartition(self, nums: List[int]) -> bool:
# Note:
# Bitmask DP (bitset)
# 1. Process root -> each number
# 2. Explore choices -> include or skip number, track sums as bits
# 3. Build -> shift bits to represent new achievable sums
# 4. Result -> check if target sum bit is set
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
# bitset where i-th bit = True if sum i achievable
bits = 1 # only 0 sum is achievable initially
for num in nums:
# Build: shift current bits by num to represent including it
bits |= bits << num
# Result: check if target sum is achievable
return (bits >> target) & 1 == 1