Dynamic Programming
Expert Answer & Key Takeaways
Dynamic Programming (DP) Mental Models
1. The Two Pillars of DP
- Overlapping Subproblems: The same subproblems are solved repeatedly (e.g., Fibonacci).
- Optimal Substructure: The optimal solution to the problem can be constructed from optimal solutions of its subproblems.
2. Implementation Strategies
A. Top-Down (Memoization)
- Approach: Recursive + Cache.
- Logic: Start from the final goal and break it down. If a subproblem is already solved, return its value from the cache.
- Pros: Intuitive, only solves necessary subproblems.
B. Bottom-Up (Tabulation)
- Approach: Iterative + Table.
- Logic: Start from the smallest subproblems (base cases) and fill the DP table until you reach the target.
- Pros: Faster (no recursion overhead), easier to optimize space.
3. Visualizing the Magic
f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1) <-- f(2) is redundant!Index: | 0 | 1 | 2 | 3 | 4 | 5 |
Value: | 0 | 1 | 1 | 2 | 3 | 5 | (dp[i] = dp[i-1] + dp[i-2])4. Space Optimization
dp[i] only depends on dp[i-1] and dp[i-2], we don't need a whole array. We can just use two variables! This reduces Space Complexity from O(N) to O(1).Longest Increasing Subsequence
nums, return the length of the longest strictly increasing subsequence.Visual Representation
nums = [10, 9, 2, 5, 3, 7, 101, 18]
LIS: [2, 3, 7, 18] or [2, 3, 7, 101]
Length: 4
DP Strategy:
dp[i] = length of LIS ending at index i
dp[i] = 1 + max(dp[j]) for all j < i AND nums[j] < nums[i]Examples
Level I: Dynamic Programming (O(N^2))
Intuition
nums[i], we look back at all previous elements nums[j]. If nums[i] > nums[j], then nums[i] can extend the increasing subsequence ending at j.Thought Process
- Initialize
dparray wheredp[i] = 1(minimum length is always 1). - For each
ifrom 1 ton-1:- For each
jfrom 0 toi-1:- If
nums[i] > nums[j]:dp[i] = max(dp[i], 1 + dp[j]).
- If
- For each
- Return the maximum value in
dp.
Detailed Dry Run
| i | nums[i] | j loop | dp table | Max reached |
|---|---|---|---|---|
| 0 | 10 | - | [1, 1, 1, 1] | 1 |
| 1 | 9 | j=0 (9<10) | [1, 1, 1, 1] | 1 |
| 2 | 2 | j=0, 1 (2<9,10) | [1, 1, 1, 1] | 1 |
| 3 | 5 | j=2 (5>2) | [1, 1, 1, 2] | 2 |
Level II: Memoization (Top-Down DP)
Intuition
memo array so that each dp[i] is computed only once.Thought Process
- Initialize
memoarray with -1. - For
solve(i), ifmemo[i] != -1, return it. - Otherwise, compute
dp[i] = 1 + max(solve(j))for allj < iwherenums[j] < nums[i]. memo[i] = dp[i].
Visual
Recursion Tree (with memoization):
solve(7): max over j < 7 where nums[j] < 18
- solve(6): 2 (cached at step 1)
- solve(5): 3 (cached at step 2)
- ... (all others are cached)Detailed Dry Run
| call | i | j checks | result |
|---|---|---|---|
| 1 | 0 | - | 1 |
| 2 | 1 | j=0 (9<10)=No | 1 |
| 3 | 2 | j=0,j=1 (2<9,10)=No | 1 |
| 4 | 3 | j=2 (5>2) | 2 (cached!) |
⚠️ Common Pitfalls & Tips
max across all solve(i) calls, don't forget that lru_cache in Python caches the function but the outer max call still needs to iterate all indices.Level III: Binary Search (O(N log N))
Intuition
tails where tails[i] is the smallest tail of all increasing subsequences of length i+1.Logic Steps
- For each
xinnums:- If
xis larger than the largest tail, append it. - Otherwise, find the smallest tail \ge x using binary search and replace it with
x.
- If
- The length of
tailsis the LIS length.
Detailed Dry Run
| x | tails | Action | Reason |
|---|---|---|---|
| 10 | [10] | Append | List empty |
| 9 | [9] | Replace 10 | 9 is smaller tail for len 1 |
| 2 | [2] | Replace 9 | 2 is smaller tail for len 1 |
| 5 | [2, 5] | Append | 5 > 2, starts len 2 |
⚠️ Common Pitfalls & Tips
tails array itself may NOT represent a valid LIS (e.g., it might be mixed from different subsequences), but its size will always be correct.Unique Paths
m x n grid. The robot is initially located at the top-left corner (grid[0][0]). The robot tries to move to the bottom-right corner (grid[m-1][n-1]). The robot can only move either down or right at any point in time.m and n, return the number of possible unique paths that the robot can take to reach the bottom-right corner.Visual Representation
m=3, n=2
Grid:
(S) (1) (S=Start, E=End)
(1) (1)
(1) (E)
Paths:
1. Right -> Down -> Down
2. Down -> Down -> Right
3. Down -> Right -> Down
Total: 3Examples
- Right -> Down -> Down
- Down -> Down -> Right
- Down -> Right -> Down
Level I: Dynamic Programming (2D)
Intuition
(i, j) is the sum of ways to reach the cell above it (i-1, j) and the cell to its left (i, j-1).Thought Process
dp[i][j] = dp[i-1][j] + dp[i][j-1].- Base cases:
dp[0][j] = 1anddp[i][0] = 1because there is only one way to move along the edges (straight right or straight down).
Detailed Dry Run
| Row | Col 0 | Col 1 | Col 2 | Action |
|---|---|---|---|---|
| 0 | 1 | 1 | 1 | Init Row 0 |
| 1 | 1 | 2 | 3 | 1+1=2, 2+1=3 |
| 2 | 1 | 3 | 6 | 1+2=3, 3+3=6 |
Level II: Memoization (Top-Down Recursion)
Intuition
(i, j) state so we never recompute it.Visual
Memo Grid (m=3, n=3):
| 0 | 1 | 1 |
| 1 | 2 | ? | <- memo[1][2] reused by (2,2)
| 1 | ? | ? |Detailed Dry Run
Level III: Space Optimized (1D)
Intuition
dp[i][j], we only need the value above it dp[i-1][j] (from the previous row) and the value to its left dp[i][j-1] (already computed in the current row). We can use a single row array.Logic Steps
- Initialize a
rowarray of sizenwith 1s. - Iterate
m-1times (for all rows except the first). - For each row, iterate through columns 1 to
n-1:row[j] = row[j] + row[j-1].
- Return
row[n-1].
Detailed Dry Run
| Action | row array |
|---|---|
| Init | [1, 1, 1] |
| Row 1 | [1, (1+1)=2, (2+1)=3] |
| Row 2 | [1, (1+2)=3, (3+3)=6] |
⚠️ Common Pitfalls & Tips
Coin Change
coins representing coins of different denominations and an integer amount representing a total amount of money.-1.Visual Representation
coins = [1, 2, 5], amount = 11
Optimal: 5 + 5 + 1 = 11 (3 coins)
DP Strategy:
To make amount 11, we can take:
- Coin 1: 1 + minCoins(10)
- Coin 2: 1 + minCoins(9)
- Coin 5: 1 + minCoins(6)
We take the minimum of these choices.Examples
Level I: Brute Force (Recursive)
Intuition
Thought Process
solve(amt) = 1 + min(solve(amt - coin))for allcoinincoins.- Base cases: If
amt == 0, return 0. Ifamt < 0, return infinity.
Pattern Identification
2+2+1, 2+1+2, 1+2+2, etc.).Detailed Dry Run
| Call | amt | Choices | Returns |
|---|---|---|---|
| 1 | 3 | 1+f(2), 1+f(1) | min(2, 2) = 2 |
| 2 | 2 | 1+f(1), 1+f(0) | min(2, 1) = 1 |
| 3 | 1 | 1+f(0), 1+f(-1) | min(1, inf) = 1 |
| 4 | 0 | - | 0 (Base Case) |
Level II: Top-Down DP (Memoization)
Intuition
amount values repeatedly. We store results in a memo map/array to skip recomputation.Visual
coins=[1,2], amount=4
tree (without memo): | with memo:
f(4) | f(4) calls f(3), f(2)
/ \ | f(3) already done in f(4)!
f(3) f(2) | Skipped!
/ \ / \ |
f(2) f(1) f(1) f(0) |Detailed Dry Run
⚠️ Common Pitfalls & Tips
-2 or INT_MAX as the uncomputed signal works well.Level III: Optimal (Bottom-Up DP)
Intuition
Thought Process
1 to targetAmount. For each amount i, we check which coin was the last one added. The minimum coins for i will be 1 + dp[i - coin].Logic Steps
- Initialize a
dparray of sizeamount + 1with a large value (e.g.,amount + 1). - Set
dp[0] = 0(base case). - Loop through each amount
ifrom 1 toamount. - For each
i, loop through eachcoin. - If
i - coin >= 0, updatedp[i] = min(dp[i], 1 + dp[i - coin]). - Return
dp[amount]or -1 if unreachable.
Detailed Dry Run
| i | Coins Loop | dp[i] calculation | Result |
|---|---|---|---|
| 1 | 1 | 1 + dp[0] = 1 | 1 |
| 2 | 1, 2 | min(1+dp[1], 1+dp[0]) | 1 |
| 3 | 1, 2 | min(1+dp[2], 1+dp[1]) | 2 |
| 4 | 1, 2 | min(1+dp[3], 1+dp[2]) | 2 |
| 5 | 1, 2, 5 | min(1+dp[4], 1+dp[3], 1+dp[0]) | 1 |
⚠️ Common Pitfalls & Tips
dp array. Using Integer.MAX_VALUE in Java or 1e9 can cause integer overflow when you do 1 + dp[i-coin]. Using amount + 1 is a safer 'infinity' for this problem.House Robber
nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.Visual Representation
nums = [1, 2, 3, 1]
Choice 1: Rob House 0 (1) + House 2 (3) = 4
Choice 2: Rob House 1 (2) + House 3 (1) = 3
Result: 4
DP Decision:
At each house, you have two choices:
1. Rob this house: nums[i] + maxLoot from houses (i-2)
2. Skip this house: maxLoot from houses (i-1)Examples
Level I: Brute Force (Recursive)
Intuition
Thought Process
rob(i) = max(nums[i] + rob(i-2), rob(i-1)).- Base cases: If index is out of bounds, return 0.
Pattern Identification
rob(3) calls rob(1) and rob(2), and rob(2) also calls rob(1)).Detailed Dry Run
| Call | i | Returns | Action |
|---|---|---|---|
| 1 | 2 | max(3+f(0), f(1)) | Recurse |
| 2 | 0 | 1 | Base case |
| 3 | 1 | max(2+f(-1), f(0)) | Recurse |
| 4 | -1 | 0 | Base case |
Level II: Memoization (Top-Down)
Intuition
rob(i) so that each house is only evaluated once, eliminating the exponential redundancy.Visual
nums = [1, 2, 3, 1]
Call Tree (Memoized):
rob(3) -> max(1 + rob(1), rob(2))
rob(2) -> max(3 + rob(0), rob(1)) | memo[2] = 4
rob(1) -> max(2 + rob(-1), rob(0)) | memo[1] = 2
rob(0) -> 1 (Base Case)Detailed Dry Run
⚠️ Common Pitfalls & Tips
Level III: Optimal (DP + Space Optimized)
Intuition
Thought Process
rob1 (max loot excluding adjacent) and rob2 (max loot including adjacent).Logic Steps
- Initialize
rob1 = 0androb2 = 0. - Iterate through each house
ninnums. temp = max(n + rob1, rob2).rob1 = rob2.rob2 = temp.- Return
rob2.
Detailed Dry Run
| i | n | rob1 | rob2 | Action |
|---|---|---|---|---|
| - | - | 0 | 0 | Init |
| 0 | 1 | 0 | 1 | max(1+0, 0) |
| 1 | 2 | 1 | 2 | max(2+0, 1) |
| 2 | 3 | 2 | 4 | max(3+1, 2) |
| 3 | 1 | 4 | 4 | max(1+2, 4) |
⚠️ Common Pitfalls & Tips
rob1 and rob2 are updated correctly in each iteration. rob1 should always be the max loot excluding the house immediately before the current one.House Robber II
nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.Visual Representation
nums = [2, 3, 2]
Circle Layout:
(2) -- (3)
| |
( ) -- (2)
Options:
1. Rob House 0: Cannot rob House 1 or House 2. Result: 2
2. Rob House 1: Cannot rob House 0 or House 2. Result: 3
Result: 3Examples
Level I: Brute Force (Recursion)
Intuition
Thought Process
rob(nums, start, end)is a recursive function for a linear range.- In circular
nums[0...n-1]:- Option 1: Rob from
0ton-2. - Option 2: Rob from
1ton-1.
- Option 1: Rob from
- Return
max(Option 1, Option 2).
Pattern Identification
Detailed Dry Run
- solve(0, 1): max(2+f(-1), f(0)) = 2. Skip last house.
- solve(1, 2): max(2+f(0), f(1)) = 2. Skip first house. Result: 2
Level II: Top-Down DP with Helper
Intuition
Visual
nums = [2, 3, 2]
Case 1: Skip last -> rob [2, 3] -> memoized linear rob
Case 2: Skip first -> rob [3, 2] -> memoized linear rob
Result: max(3, 3) = 3Detailed Dry Run
⚠️ Common Pitfalls & Tips
lru_cache in Python captures closure variables, use tuples as arguments for the circular-to-linear reduction to avoid cache collisions between the two sub-problems.Level III: Dynamic Programming (Linear Sub-problems)
Intuition
- Rob houses from index
0ton-2(exclude last). - Rob houses from index
1ton-1(exclude first).
Thought Process
- If
n=1, returnnums[0]. - Calculate
maxLoot1 = houseRobberLinear(nums[0...n-2]). - Calculate
maxLoot2 = houseRobberLinear(nums[1...n-1]). - Return
max(maxLoot1, maxLoot2).
Pattern Identification
Detailed Dry Run
| Step | Subset | Max Loot (Linear) |
|---|---|---|
| 1 | [2, 3] | max(2, 3) = 3 |
| 2 | [3, 2] | max(3, 2) = 3 |
| Final | max(3, 3) | 3 |
⚠️ Common Pitfalls & Tips
n=1. If you don't handle it, slicing or indexing might fail. Also, ensure both linear calls exclude at least one house to break the circular dependency.Jump Game
nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.true if you can reach the last index, or false otherwise.Visual Representation
nums = [2, 3, 1, 1, 4]
Index: 0 1 2 3 4
Value: 2 3 1 1 4
- From 0 (2): Can jump to 1 or 2.
- From 1 (3): Can jump to 2, 3, or 4 (Goal!).
Result: TrueExamples
Level I: Dynamic Programming (Bottom-Up)
Intuition
i is reachable if there exists some j < i such that j is reachable AND j + nums[j] >= i.Thought Process
canReach[i]= true if indexiis reachable.canReach[0] = true.- For each
i, check allj < ito see if a jump can land oni.
Pattern Identification
Detailed Dry Run
| i | nums[i] | j loop | canReach table | Status |
|---|---|---|---|---|
| 0 | 2 | - | [T, F, F, F, F] | Start |
| 1 | 3 | j=0 (0+2 >= 1) | [T, T, F, F, F] | OK |
| 2 | 1 | j=0 (0+2 >= 2) | [T, T, T, F, F] | OK |
| 4 | 4 | j=1 (1+3 >= 4) | [T, T, T, T, T] | REACHED |
Level II: Top-Down DP (Memoization)
Intuition
j < i. We can speed it up by also keeping a track of the farthest reachable index instead of building the full table — but for memoization we cache 'can we reach this index from the start?'.Visual
nums = [2, 3, 1, 1, 4]
memo[0] = True, memo[1] = ?, memo[2] = ?
- Check: can we reach index 1? From 0, jump 1 or 2. Yes!
- Can we reach index 4? From 1, jump 3. Yes! memo[4] = True.Detailed Dry Run
⚠️ Common Pitfalls & Tips
Level III: Optimal (Greedy)
Intuition
i, we can also reach everything before it.Logic Steps
- Initialize
goal = nums.length - 1. - Iterate through the array from right to left.
- If
i + nums[i] >= goal, then indexican reach the goal. Update thegoal = i. - If at the end
goal == 0, return true.
Detailed Dry Run
| i | nums[i] | i + nums[i] | Goal | Update? |
|---|---|---|---|---|
| 4 | 4 | 8 | 4 | Init |
| 3 | 1 | 4 | 4 | 3+1 >= 4 (YES), Goal=3 |
| 2 | 1 | 3 | 3 | 2+1 >= 3 (YES), Goal=2 |
| 1 | 3 | 4 | 2 | 1+3 >= 2 (YES), Goal=1 |
| 0 | 2 | 2 | 1 | 0+2 >= 1 (YES), Goal=0 |
⚠️ Common Pitfalls & Tips
Longest Common Subsequence
text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.Visual Representation
text1 = "abcde", text2 = "ace"
LCS is "ace", Length = 3
DP Grid Logic:
- If text1[i] == text2[j]:
1 + dp[i+1][j+1] (Match!)
- Else:
max(dp[i+1][j], dp[i][j+1]) (Skip one char)Examples
Level I: Brute Force (Recursive)
Intuition
text1 or skip a character from text2. We take the maximum of these two paths.Thought Process
solve(i, j): LCS oftext1[i:]andtext2[j:].- If
text1[i] == text2[j]:1 + solve(i+1, j+1). - Else:
max(solve(i+1, j), solve(i, j+1)).
Pattern Identification
Detailed Dry Run
| i | j | Match? | Action | Returns |
|---|---|---|---|---|
| 0 | 0 | 'a'=='a' (Yes) | 1 + f(1, 1) | 1 + 2 = 3 |
| 1 | 1 | 'b'=='c' (No) | max(f(2, 1), f(1, 2)) | max(1, 2) = 2 |
| 1 | 2 | 'b'=='e' (No) | max(f(2, 2), f(1, 3)) | max(1, 0) = 1 |
Level II: Memoization (Top-Down 2D)
Intuition
(i, j) pairs many times. We add a 2D memo table indexed by string positions.Visual
text1="abc", text2="ace"
Memo table after caching:
"" 'a' 'c' 'e'
"" 0 0 0 0
'a' 0 [3] - - <- solve(0,0)=3 cached!
'b' 0 - - -
'c' 0 - [1] -Detailed Dry Run
⚠️ Common Pitfalls & Tips
lru_cache automatically creates the 2D memo table. In Java, a 2D array initialized with -1 suffices for string indices.Level III: Optimal (2D DP)
Intuition
Thought Process
dp[i][j] represents the LCS length for text1[i:] and text2[j:]. We fill the grid from the bottom-right towards the top-left (or top-left to bottom-right).Logic Steps
- Initialize a 2D array
dpof size(m+1) x (n+1)with 0s. - Iterate through
i(text1) andj(text2) in reverse. - If characters match:
dp[i][j] = 1 + dp[i+1][j+1]. - Else:
dp[i][j] = max(dp[i+1][j], dp[i][j+1]). - Return
dp[0][0].
Detailed Dry Run
| Row (i) | Col (j) | Match? | dp[i][j] calculation | Value |
|---|---|---|---|---|
| 2 (c) | 2 (e) | No | max(dp[3][2], dp[2][3]) | 0 |
| 2 (c) | 1 (c) | Yes | 1 + dp[3][2] | 1 |
| 1 (b) | 1 (c) | No | max(dp[2][1], dp[1][2]) | 1 |
| 0 (a) | 0 (a) | Yes | 1 + dp[1][1] | 3 |
⚠️ Common Pitfalls & Tips
dp table with size (n+1) x (m+1) to handle the base cases (empty strings) easily without extra boundary checks.Word Break
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.Visual Representation
s = "leetcode", wordDict = ["leet", "code"]
s[0:4] = "leet" (Found!)
s[4:8] = "code" (Found!)
Result: True
DP Strategy:
dp[i] is true if s[0...i] can be segmented.
dp[i] = true if EXISTS j < i such that dp[j] is true AND s[j...i] is in wordDict.Examples
Level I: Brute Force (Recursion)
Intuition
Thought Process
- For a string
s, iterateifrom 1 tolen(s). - If
s[0:i]is inwordDict:- Recursively call
canBreak(s[i:]).
- Recursively call
- Base case: If the remaining string is empty, return
true.
Detailed Dry Run
- prefix "a" in Dict? Yes. Recurse on "bc".
- prefix "b" in Dict? No.
- prefix "bc" in Dict? Yes. Recurse on "".
- Base case "" -> True.
Level II: Memoization (Top-Down DP)
Intuition
Visual
s = "leetcode", dict = ["leet", "code"]
memo[0]=T -> "leet" matches -> memo[4]=?
memo[4]=T -> "code" matches -> memo[8]=TDetailed Dry Run
⚠️ Common Pitfalls & Tips
Set for dictionary lookups (O(1)) instead of a list (O(L)). Without this, the overall complexity worsens significantly.Level III: Dynamic Programming (Bottom-Up)
Intuition
s[0...j] can be segmented (indicated by dp[j] = true), we then check if the remaining substring s[j...i] exists in the dictionary.Thought Process
- Initialize
dparray of sizen+1withfalse. dp[0] = true(empty string is always valid).- For
ifrom 1 ton:- For
jfrom 0 toi-1:- If
dp[j]is true ANDs.substring(j, i)is inwordDict:- Set
dp[i] = trueandbreak.
- Set
- If
- For
- Return
dp[n].
Detailed Dry Run
| i | j | Substring | dp[j] | Solution |
|---|---|---|---|---|
| 0 | - | - | T | Base Case |
| 4 | 0 | "leet" | T | dp[4] = True |
| 8 | 4 | "code" | T | dp[8] = True |
⚠️ Common Pitfalls & Tips
Set for dictionary lookup improves lookup time from O(W) to O(1) average.Partition Equal Subset Sum
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.Visual Representation
nums = [1, 5, 11, 5]
Total Sum = 22, Target = 11
Subsets:
[1, 5, 5] -> Sum 11
[11] -> Sum 11
Result: True
Logic:
Can we find a subset with sum equal to (TotalSum / 2)?Examples
Level I: Dynamic Programming (Bottom-Up 2D)
Intuition
sum = totalSum / 2.Thought Process
- If
totalSumis odd, returnfalse(cannot partition equally). - Target =
totalSum / 2. dp[i][j]= true if sumjcan be formed using firstielements.dp[i][j] = dp[i-1][j](exclude current) ORdp[i-1][j - nums[i]](include current, ifj >= nums[i]).
Detailed Dry Run
| i | x | Sum 0 | Sum 1 | Sum 2 | Sum 3 | Action |
|---|---|---|---|---|---|---|
| 0 | 1 | T | T | F | F | Init |
| 1 | 2 | T | T | T | T | dp[0][3] |
| Result | T | True! |
Level II: Memoization (Top-Down)
Intuition
Visual
nums=[1,5,11,5], target=11
solve(4, 11): can_pick(11)? No, but 11<11 is false for nums[3]=5
solve(3, 6): can_pick(11)? ...Detailed Dry Run
⚠️ Common Pitfalls & Tips
i and t as indices is faster and preferred in practice.Level III: Space Optimized (1D)
Intuition
dp[i][j] only depends on the previous row dp[i-1], we can use a single row array. To avoid using the same element twice for the same sum (0/1 constraint), we iterate the sum backwards.Logic Steps
- Initialize a
dparray of sizetarget + 1withfalse. dp[0] = true.- For each
numinnums:- For
jfromtargetdown tonum:dp[j] = dp[j] || dp[j - num].
- For
Detailed Dry Run
| num | Target 3 | Target 2 | Target 1 | Target 0 |
|---|---|---|---|---|
| - | F | F | F | T |
| 1 | F | F | T (0+1) | T |
| 2 | T (1+2) | T (0+2) | T | T |
| 3 | T (0+3) | T | T | T |
⚠️ Common Pitfalls & Tips
target to num). If you iterate forwards, you might use the same element multiple times for the same sum (which is unbounded knapsack behavior, not 0/1).Climbing Stairs
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?Visual Representation
n = 3
Ways:
1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step
Total: 3 ways
Recursion Tree:
(3)
/ \
(2) (1)
/ \ |
(1) (0) (0)Examples
- 1 step + 1 step
- 2 steps
- 1 + 1 + 1
- 1 + 2
- 2 + 1
Level I: Brute Force (Recursion)
Intuition
n, you could have come from either step n-1 (by taking 1 step) or step n-2 (by taking 2 steps). Thus, the total ways . This is exactly the Fibonacci sequence.Thought Process
- Base cases: If or , there is only 1 way.
- Recursive step: Return .
Pattern Identification
Detailed Dry Run
| Call | n | Returns | Action |
|---|---|---|---|
| 1 | 3 | f(2) + f(1) | Recurse |
| 2 | 2 | f(1) + f(0) | Recurse |
| 3 | 1 | 1 | Base Case |
| 4 | 0 | 1 | Base Case |
Level II: Memoization (Top-Down)
Intuition
climb(n) using a memo table so each stair count is computed only once.Visual
climb(5)
climb(4) --cached after first call!
climb(3) --cached after first call!
...
Only N unique calls made.Detailed Dry Run
⚠️ Common Pitfalls & Tips
lru_cache handles the caching automatically. In other languages, a HashMap or array of size N+1 is needed.Level III: Optimal (DP + Space Optimization)
Intuition
Thought Process
Logic Steps
- Initialize
prev1 = 1(for step 1) andprev2 = 1(for step 0). - Loop from 2 to
n. - Calculate
current = prev1 + prev2. - Update
prev2 = prev1andprev1 = current.
Detailed Dry Run
| i | current | prev1 | prev2 | Action |
|---|---|---|---|---|
| - | - | 1 | 1 | Init |
| 2 | 1+1=2 | 2 | 1 | Update variables |
| 3 | 2+1=3 | 3 | 2 | Update variables |
⚠️ Common Pitfalls & Tips
Regular Expression Matching
s and a pattern p, implement regular expression matching with support for . and * where:.Matches any single character.*Matches zero or more of the preceding element.
Visual Representation
s = "aa", p = "a*"
- 'a*' can match "", "a", "aa", "aaa", etc.
- Since it matches "aa", result is True.
s = "ab", p = ".*"
- '.*' matches anything. Result is True.Examples
Level I: Brute Force (Recursion)
Intuition
Thought Process
- If
pis empty, returntrueifsis also empty. - Check if the first characters match (
s[0] == p[0]orp[0] == '.'). - If
p[1] == '*':- Either skip '*' and preceding char (zero match):
isMatch(s, p[2:]). - Or if first chars match, consume one char of
sand keep pattern:first_match && isMatch(s[1:], p).
- Either skip '*' and preceding char (zero match):
- Else: If first chars match, recurse on
isMatch(s[1:], p[1:]).
Detailed Dry Run
- p[1] is '*'. Try skip: isMatch("aa", "") -> False.
- Try consume: 'a'=='a', so isMatch("a", "a*").
- Again '', try consume: 'a'=='a', so isMatch("", "a").
- Again '*', try skip: isMatch("", "") -> True.
Level II: Memoization (Top-Down 2D)
Intuition
(i, j) states. Adding a 2D memo array indexed by (si, pi) eliminates repeated work.Visual
s="aa", p="a*"
solve(0,0): p[1]='*' -> zero match: solve(0,2); one+ match: solve(1,0)
solve(1,0): cached after first call!Detailed Dry Run
⚠️ Common Pitfalls & Tips
pi+2), then only try 'one+ match' if firstMatch is true to avoid infinite loops.Level III: Dynamic Programming (Bottom-Up 2D)
Intuition
dp[i][j] to represent if s[0...i-1] matches p[0...j-1].Thought Process
dp[0][0] = true(empty string matches empty pattern).- Pattern with '*':
- Case 1: '*' matches zero characters:
dp[i][j] = dp[i][j-2]. - Case 2: '*' matches one or more:
dp[i][j] = dp[i-1][j]ifs[i-1]matchesp[j-2].
- Case 1: '*' matches zero characters:
- Pattern without '*':
- If
s[i-1]matchesp[j-1](either same char or '.'),dp[i][j] = dp[i-1][j-1].
- If
Detailed Dry Run
| i\j | "" | 'a' | '*' |
|---|---|---|---|
| "" | T | F | T (zero 'a') |
| 'a' | F | T | T (one 'a') |
| 'a' | F | F | T (two 'a') |
⚠️ Common Pitfalls & Tips
a*b*c* is tricky. Ensure you initialize dp[0][j] correctly for '*' characters.Burst Balloons
n balloons, indexed from 0 to n - 1. Each balloon is painted with a number on it, represented by an array nums. You are asked to burst all the balloons.i-th balloon, you will get nums[i - 1] * nums[i] * nums[i + 1] coins. After bursting it, the i - 1-th and i + 1-th balloons become adjacent.nums[-1] = nums[n] = 1).Visual Representation
nums = [3, 1, 5, 8]
Add boundary: [1, 3, 1, 5, 8, 1]
If we burst 1 first:
Coins = 3 * 1 * 5 = 15
Remaining: [1, 3, 5, 8, 1]
Wait! It's better to think: "Which balloon is burst LAST in range (i, j)?"Examples
Level I: Brute Force (Recursion)
Intuition
k to burst last in the interval (i, j). This splits the problem into two subproblems: bursting balloons in (i, k) and (k, j). Try all possible candidates for the last balloon.Thought Process
- Pad the array with 1 at both ends.
- Define a function
solve(i, j)that returns max coins from interval[i, j]. - Base case: If
i > j, return 0. - Recursive step:
max(solve(i, k-1) + solve(k+1, j) + A[i-1]*A[k]*A[j+1])for allkin[i, j].
Detailed Dry Run
Level II: Memoization (Top-Down Interval DP)
Intuition
(i, j) intervals. We add a 2D memo table to cache the max coins for each interval.Visual
nums=[3,1,5,8], A=[1,3,1,5,8,1]
memo[1][4] = max coins for range (1..4)
After first computation, reused whenever (1,4) is subproblem!Detailed Dry Run
⚠️ Common Pitfalls & Tips
Level III: Dynamic Programming (Interval/Matrix Chain)
Intuition
k is burst last in the interval (i, j). This allows us to split the problem into two independent subproblems: bursting balloons in (i, k) and (k, j).Thought Process
- Pad the array with
1at both ends. dp[i][j]= max coins from bursting balloons between indexiandj(exclusive).- For each length
lenfrom 1 ton:- For each start
ifrom 1 ton - len + 1:- End
j = i + len - 1. - For
kfromitoj(last balloon to burst):coins = nums[i-1] * nums[k] * nums[j+1] + dp[i][k-1] + dp[k+1][j].
- End
- For each start
Detailed Dry Run
| Interval | Last k | Calculation | Result |
|---|---|---|---|
| (1,1) | 1 | 131 + 0 + 0 | 3 |
| (2,2) | 2 | 315 + 0 + 0 | 15 |
| (3,3) | 3 | 551 + 0 + 0 | 25 |
| (1,2) | 1 | 135 + 0 + dp[2][2] = 15+15 | 30 |
| (1,2) | 2 | 115 + dp[1][1] + 0 = 5+3 | 8 |
⚠️ Common Pitfalls & Tips
Longest Valid Parentheses
Visual Representation
s = ")()())"
Substrings:
() -> 2
()() -> 4 (Max)
DP Strategy:
dp[i] = length of longest valid parentheses ending at index i.Examples
Level I: Dynamic Programming
Intuition
dp[i] is the length of the longest valid parentheses ending at index i.Thought Process
- If
s[i] == '(',dp[i] = 0(valid substring can't end with open paren). - If
s[i] == ')':- If
s[i-1] == '(', thendp[i] = dp[i-2] + 2. - If
s[i-1] == ')'ands[i - dp[i-1] - 1] == '(', thendp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2.
- If
- Keep track of maximum
dp[i].
Detailed Dry Run
| i | char | Calculation | dp[i] |
|---|---|---|---|
| 0 | ( | - | 0 |
| 1 | ) | dp[0-1]+2 = 2 | 2 |
| 2 | ( | - | 0 |
| 3 | ) | dp[3-2]+2 = dp[1]+2 = 4 | 4 |
Level II: Stack-Based Approach
Intuition
i - stack.peek().Visual
s = ")()())"
stack: [-1]
i=0 ')': stack = [] -> push 0? No, stack is empty -> push i=0
i=1 '(': stack = [0, 1]
i=2 ')': pop 1 -> stack = [0], len = 2-0=2
i=3 '(': stack = [0, 3]
i=4 ')': pop 3 -> stack = [0], len = 4-0=4
i=5 ')': pop 0 -> stack = [], push 5
Max = 4Detailed Dry Run
⚠️ Common Pitfalls & Tips
Level III: Two Counters (Optimal Space)
Intuition
left and right parentheses counts.Logic Steps
- Scan Left to Right:
left++for '(',right++for ')'.- If
left == right, current length is2 * left. Updatemax. - If
right > left, reset both to 0.
- Scan Right to Left:
- Repeat same logic but reset if
left > right.
- Repeat same logic but reset if
Detailed Dry Run
- L-R: '('(L1,R0), '('(L2,R0), ')'(L2,R1). L!=R. Max=0.
- R-L: ')'(L0,R1), '('(L1,R1) -> Max=2, '('(L2,R1) -> Reset. Result: 2.
Edit Distance
word1 and word2, return the minimum number of operations required to convert word1 to word2.- Insert a character
- Delete a character
- Replace a character
Visual Representation
word1 = "horse", word2 = "ros"
Steps:
1. horse -> rorse (replace 'h' with 'r')
2. rorse -> rose (remove 'r')
3. rose -> ros (remove 'e')
Result: 3
DP Strategy:
dp[i][j] = min distance between word1[0...i] and word2[0...j]Examples
Level I: Dynamic Programming (2D)
Intuition
dp[i][j] is the minimum operations to convert word1[0...i] to word2[0...j].Thought Process
- If
word1[i-1] == word2[j-1], no operation is needed for the current character:dp[i][j] = dp[i-1][j-1]. - If they are different, we take the minimum of three operations:
- Insert:
1 + dp[i][j-1] - Delete:
1 + dp[i-1][j] - Replace:
1 + dp[i-1][j-1]
- Insert:
- Base cases: Converting string to empty string requires
i(orj) deletions.
Detailed Dry Run
| i\j | "" | 'a' | 'b' | 'd' |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| 'a' | 1 | 0 (match) | 1 | 2 |
| 'b' | 2 | 1 | 0 (match) | 1 |
| 'c' | 3 | 2 | 1 | 1 (replace 'c' with 'd') |
Level II: Memoization (Top-Down 2D)
Intuition
(i, j). Cache results to avoid recomputation.Visual
word1='horse', word2='ros'
solve(0,0): 'h'!='r' -> min(solve(1,1), solve(1,0), solve(0,1))
solve(1,1): cached after first call!Detailed Dry Run
Level III: Space Optimized (1D)
Intuition
dp[i][j], we only need the current row and the previous row. Specifically, dp[i-1][j-1] (diagonal), dp[i-1][j] (up), and dp[i][j-1] (left).Logic Steps
- Initialize
currarray of sizen+1with base case values0...n. - Iterate through
word1. For eachi:- Cache
dp[i-1][j-1]in aprevvariable. - Update the
curr[0]for the base case (conversions from word1[0...i] to empty string). - Inner loop through
word2to updatecurr[j].
- Cache
- Return
curr[n].
Detailed Dry Run
Frog Jump
k units, then its next jump must be either k - 1, k, or k + 1 units. Note that the frog can only jump in the forward direction.Visual Representation
Stones: [0, 1, 3, 5, 6, 8, 12, 17]
Jump 1: 0 -> 1 (Size 1)
Jump 2: 1 -> 3 (Size 2, allowed: 1-1, 1, 1+1)
Jump 3: 3 -> 5 (Size 2, allowed: 1, 2, 3)
...Examples
Level I: Brute Force (Recursion)
Intuition
i reached with a jump of k, the next jump can be k-1, k, or k+1.Thought Process
- Start at the first stone.
- From stone at position
pos, if we reached it with jumpk, try positionspos + k - 1,pos + k, andpos + k + 1. - Base case: If we reach the last stone, return
true. - If the target position has a stone, recurse.
Detailed Dry Run
- Start at 0. Only way to start is jump 1 to stone 1.
- At 1, last jump was 1. Next jumps: 0 (skip), 1, 2.
- Try k=2: 1 + 2 = 3. Stone 3 exists! Reached target.
Level II: Memoization (Top-Down)
Intuition
(position, lastJump) states to avoid recomputing the same frog position visited with the same jump size.Visual
stones=[0,1,3,5,6], target=6
solve(1, 1): -> solve(3, 2) or solve(3, 1)
solve(3,2): cached after first call!Detailed Dry Run
Level III: Dynamic Programming (Map of Sets)
Intuition
Thought Process
- Initialize
Map<Integer, Set<Integer>>. For each stone, create an empty set. - Add jump size
0to the first stone (base case). - For each stone
s:- For each jump size
kinmap.get(s):- For each next jump
stepin{k-1, k, k+1}:- If
step > 0andmap.containsKey(s + step):- Add
stepto the set for stones + step.
- Add
- If
- For each next jump
- For each jump size
- If the set for the last stone is not empty, return
true.
Detailed Dry Run
| Stone | Jump Sizes |
|---|---|
| 0 | {1} (Initial jump is 1) |
| 1 | {1} (from 0+1) |
| 3 | {2} (from 1+2, k=1 -> k+1=2) |
| Result: True (Set for 3 is not empty) |
Minimum Path Sum in Grid
m x n grid filled with non-negative numbers, find a path from top-left to bottom-right, which minimizes the sum of all numbers along its path.Visual Representation
Grid:
[1, 3, 1]
[1, 5, 1]
[4, 2, 1]
Paths:
1-3-1-1-1 = 7 (Min)
1-1-5-2-1 = 10
1-1-4-2-1 = 9Examples
Level I: Dynamic Programming (2D)
Intuition
(i, j) is the value of the current cell plus the minimum of the paths reaching from above (i-1, j) or from the left (i, j-1).Thought Process
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]).- Edges: For the first row, you can only come from the left. For the first column, you can only come from above.
- Base Case:
dp[0][0] = grid[0][0].
Detailed Dry Run
| Cell | Calculation | Sum |
|---|---|---|
| (0, 0) | Base | 1 |
| (0, 1) | 1 + grid[0][1] = 1+3 | 4 |
| (1, 0) | 1 + grid[1][0] = 1+1 | 2 |
| (1, 1) | 5 + min(dp[0][1], dp[1][0]) = 5 + min(4, 2) | 7 |
Level II: Memoization (Top-Down Recursion)
Intuition
(i, j) state so we never recompute it.Visual
Memo Grid (m=3, n=3):
| 0 | 1 | 1 |
| 1 | 2 | ? | <- memo[1][2] reused by (2,2)
| 1 | ? | ? |Detailed Dry Run
⚠️ Common Pitfalls & Tips
Level III: Space Optimized (1D)
Intuition
dp[i][j], we only need the value above it (previous row) and the value to its left (current row). We can use a single array of size n.Logic Steps
- Initialize
rowarray of sizenwithInfinity. - Set
row[0] = 0(starting point for accumulation). - For each
i(row):row[0] += grid[i][0]- For each
j(col from 1 ton-1):row[j] = grid[i][j] + min(row[j], row[j-1]).
- Return
row[n-1].
Detailed Dry Run
| Row | row array calculation | State |
|---|---|---|
| 0 | [1, 1+3=4] | [1, 4] |
| 1 | [1+1=2, 5+min(2, 4)=7] | [2, 7] |
Maximal Square
m x n binary matrix filled with 0's and 1's, find the largest square containing only 1's and return its area.Visual Representation
Matrix:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
The 2x2 square of 1s at (1,2) to (2,3) is one of the squares.
Wait, there is a larger one? No, area is 4.
DP Strategy:
dp[i][j] = side length of the largest square ending at (i, j).Examples
Level I: Dynamic Programming (2D)
Intuition
k ending at (i, j) can only be formed if there are squares of size k-1 ending at (i-1, j), (i, j-1), and (i-1, j-1).Thought Process
dp[i][j]represents the side length of the largest square ending atmatrix[i][j].- If
matrix[i][j] == '1':dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1.
- Keep track of the maximum side length found so far.
- Result =
maxSide * maxSide.
Detailed Dry Run
| i\j | 0 | 1 |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 1 | min(1, 1, 1)+1 = 2 |
| Result: 2^2 = 4 |
Level II: Memoization (Top-Down)
Intuition
dp(i, j) so each cell is computed only once. The recurrence: dp(i,j) = min(dp(i-1,j), dp(i,j-1), dp(i-1,j-1)) + 1 if matrix[i][j]=='1'.Visual
Matrix: Memo:
1 1 1 1 1 1
1 1 1 -> 1 2 ?
1 1 1 1 ? 3 <- dp(2,2) = min(2,2,2)+1=3Detailed Dry Run
Level III: Space Optimized (1D)
Intuition
dp[i-1][j], dp[i][j-1], and the diagonal dp[i-1][j-1].Logic Steps
- Initialize a
dparray of sizen + 1. - Use a
prevvariable to store the diagonal valuedp[i-1][j-1]from the previous row. - Iterate row by row.
Detailed Dry Run
Palindrome Partitioning II
s, partition s such that every substring of the partition is a palindrome.s.Visual Representation
s = "aab"
Valid partitions:
["a", "a", "b"] -> 2 cuts
["aa", "b"] -> 1 cut (Min)
Result: 1Examples
Level I: Brute Force (Recursion)
Intuition
Thought Process
solve(i)returns min cuts fors[i...n-1].- Base case: If
s[i...n-1]is a palindrome, return 0 (no more cuts needed). - For each
jfromiton-1:- If
s[i...j]is a palindrome:res = min(res, 1 + solve(j + 1)).
- If
Detailed Dry Run
- "a" is pal. solve("ab")
- "aa" is pal. solve("b") -> "b" is pal, returns 0. Total 1+0=1. Result: 1
Level II: Memoization (Top-Down DP)
Intuition
solve(i) (min cuts for suffix s[i:]) using a memo array. Also precompute palindrome check to avoid O(N) palindrome check every time.Visual
s = "aab"
memo[2] = 0 ("b" is palindrome)
memo[1] = 1 ("ab" -> "a"|"b" = 1 cut, or "ab" not pal)
memo[0] = 1 ("aa"|"b" = 1 cut, memo[2] cached!)Detailed Dry Run
Level III: Dynamic Programming (Two Passes)
Intuition
isPal[i][j] to precompute if s[i...j] is a palindrome. Another 1D array cuts[i] to store the minimum cuts needed for s[0...i].Thought Process
- Precompute
isPal[i][j]using standard palindrome DP:s[i] == s[j] && (j-i < 2 || isPal[i+1][j-1]). - Initialize
cuts[i] = i(max possible cuts: each char separate). - For
jfrom 0 ton-1:- If
isPal[0][j]is true,cuts[j] = 0(no cuts needed for entire prefix). - Else, for
ifrom 1 toj:- If
isPal[i][j]is true,cuts[j] = min(cuts[j], cuts[i-1] + 1).
- If
- If
Detailed Dry Run
| j | isPal(0,j) | Action | cuts[j] |
|---|---|---|---|
| 0 | T | 0 cuts | 0 |
| 1 | T | 0 cuts | 0 |
| 2 | F | cuts[1]+1 ("aa" | "b") |
| Result: 1 |
Interleaving String
s1, s2, and s3, find whether s3 is formed by an interleaving of s1 and s2.s and t is a configuration where they are divided into non-empty substrings such that:s = s1 + s2 + ... + snt = t1 + t2 + ... + tm- The interleaving is
s1 + t1 + s2 + t2 + s3 + t3 + ...ort1 + s1 + t2 + s2 + t3 + s3 + ...
Visual Representation
s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
Characters from s1 (bold), s2 (italic):
a a d b b c b c a c
(a)(a)(d)(b)(b)(c)(b)(c)(a)(c)
s1 s1 s2 s2 s2 s1 s2 s1 s2 s1
Result: TrueExamples
Level I: Dynamic Programming (Bottom-Up 2D)
Intuition
dp[i][j] to represent if s3[0...i+j-1] is formed by interleaving s1[0...i-1] and s2[0...j-1].Thought Process
dp[i][j]is true if:dp[i-1][j]is true ANDs1[i-1] == s3[i+j-1]- OR
dp[i][j-1]is true ANDs2[j-1] == s3[i+j-1]
- Base case:
dp[0][0] = true. - Base case:
dp[i][0]matches only againsts1.dp[0][j]matches only againsts2.
Detailed Dry Run
| i\j | "" | 'b' |
|---|---|---|
| "" | T | F (s2[0]!=s3[0]) |
| 'a' | T (s1[0]==s3[0]) | T (s1[0]==s3[0] or s2[0]==s3[1]) |
| Result: True |
Level II: Memoization (Top-Down 2D)
Intuition
Visual
s1="a", s2="b", s3="ab"
solve(0,0): s1[0]='a'==s3[0] -> solve(1,0)
or s2[0]='b'!=s3[0] -> blocked
solve(1,0): s2[0]='b'==s3[1] -> solve(1,1)=TrueDetailed Dry Run
Level III: Space Optimized (1D)
Intuition
dp[i][j] only depends on dp[i-1][j] and dp[i][j-1], we can reduce space to O(M) (where M is length of s2).Logic Steps
- Initialize a
dparray of sizes2.length + 1. - In the outer loop (s1),
dp[j]will representdp[i][j]. dp[j]is updated as(dp[j] && s1[i-1] == s3[i+j-1]) || (dp[j-1] && s2[i-1] == s3[i+j-1]).
Detailed Dry Run
Minimum Cost to Merge Stones
n piles of stones arranged in a row. The i-th pile has stones[i] stones.k consecutive piles into one pile, and the cost of this move is equal to the total number of stones in these k piles.Visual Representation
stones = [3, 2, 4, 1], k = 2
[3, 2, 4, 1] -> [5, 4, 1] (cost 5)
[5, 4, 1] -> [5, 5] (cost 5)
[5, 5] -> [10] (cost 10)
Total Cost: 20 (Min)Examples
Level I: Brute Force (Recursion)
Intuition
[i, j] to merge into m piles. This is a classic exhaustive search for partition problems.Thought Process
solve(i, j, piles)returns min cost to mergestones[i...j]intopilespiles.- Transitions:
- To get
pilespiles from[i, j], we can split into[i, k](merged into1pile) and[k+1, j](merged intopiles-1piles). solve(i, j, piles) = min(solve(i, k, 1) + solve(k + 1, j, piles - 1)).
- To get
- Base case:
solve(i, i, 1) = 0.
Detailed Dry Run
- solve(0, 2, 1) = solve(0, 2, 3) + sum(3, 2, 4).
- solve(0, 2, 3) = split into [0,0] (1 pile) and [1,2] (2 piles).
- Result: 9.
Level II: Memoization (Top-Down 3D)
Intuition
solve(i, j, m) — the minimum cost to merge stones[i..j] into m piles. This avoids recomputing the same interval+pile-count combinations.Visual
solve(0,3,1) [merge ALL into 1 pile]
-> solve(0,1,1) + solve(2,3,1) [split and merge]
Both sub-calls cached after first computation!Detailed Dry Run
Level III: Dynamic Programming (Interval/3D)
Intuition
[i, j] into p piles.Thought Process
dp[i][j][m]means the min cost to merge stones instones[i...j]intompiles.- Transitions:
- To merge
[i, j]intompiles, we can split it into[i, k](merged into1pile) and[k+1, j](merged intom-1piles). dp[i][j][m] = min(dp[i][k][1] + dp[k+1][j][m-1])forkin[i, j-1].- Base Case:
dp[i][i][1] = 0(single pile is already 1 pile).
- To merge
- When
m == 1, the cost isdp[i][j][k] + sum(stones[i...j]).
Detailed Dry Run
dp[i][j][m] helps track number of piles remaining, which is crucial because we can only merge EXACTLY K piles at once.Course4All Technical Board
Verified ExpertSenior Software Engineers & Algorithmic Experts
Our DSA content is authored and reviewed by engineers from top tech firms to ensure optimal time and space complexity analysis.
Found an issue or have a suggestion?
Help us improve! Report bugs or suggest new features on our Telegram group.