feat(content): test cases batch 1
This commit is contained in:
@@ -9,82 +9,158 @@ categories:
|
||||
patterns:
|
||||
- dynamic-programming
|
||||
|
||||
description: |
|
||||
You are climbing a staircase. It takes n steps to reach the top.
|
||||
function_signature: "def climb_stairs(n: int) -> int:"
|
||||
|
||||
Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
|
||||
test_cases:
|
||||
visible:
|
||||
- input: { n: 2 }
|
||||
expected: 2
|
||||
- input: { n: 3 }
|
||||
expected: 3
|
||||
hidden:
|
||||
- input: { n: 1 }
|
||||
expected: 1
|
||||
- input: { n: 4 }
|
||||
expected: 5
|
||||
- input: { n: 5 }
|
||||
expected: 8
|
||||
- input: { n: 10 }
|
||||
expected: 89
|
||||
|
||||
description: |
|
||||
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?
|
||||
|
||||
constraints: |
|
||||
- 1 <= n <= 45
|
||||
- `1 <= n <= 45`
|
||||
|
||||
examples:
|
||||
- input: "n = 2"
|
||||
output: "2"
|
||||
explanation: "Two ways: (1+1) or (2)"
|
||||
explanation: "There are two ways: (1 step + 1 step) or (2 steps)."
|
||||
- input: "n = 3"
|
||||
output: "3"
|
||||
explanation: "Three ways: (1+1+1), (1+2), (2+1)"
|
||||
explanation: "There are three ways: (1+1+1), (1+2), or (2+1)."
|
||||
|
||||
explanation:
|
||||
approach: |
|
||||
1. Recognize this follows the Fibonacci pattern
|
||||
2. ways(n) = ways(n-1) + ways(n-2)
|
||||
3. From step n-1, we can take 1 step to reach n
|
||||
4. From step n-2, we can take 2 steps to reach n
|
||||
5. Base cases: ways(1) = 1, ways(2) = 2
|
||||
|
||||
intuition: |
|
||||
At any step, you either got there by taking 1 step from the previous position
|
||||
or 2 steps from two positions back. This gives us the recurrence relation.
|
||||
Imagine standing on a step and thinking backwards: "How did I get here?" You either took 1 step from the previous step, or you took 2 steps from two steps back. There's no other way!
|
||||
|
||||
This is essentially the Fibonacci sequence! The number of ways to reach step n
|
||||
equals the sum of ways to reach steps n-1 and n-2.
|
||||
Think of it like this: the number of ways to reach step `n` is the **sum** of:
|
||||
- Ways to reach step `n-1` (then take 1 step)
|
||||
- Ways to reach step `n-2` (then take 2 steps)
|
||||
|
||||
This gives us the recurrence: `ways(n) = ways(n-1) + ways(n-2)`
|
||||
|
||||
If this looks familiar, it should — this is the **Fibonacci sequence**! The number of ways to climb n stairs equals the n<sup>th</sup> Fibonacci number (with slightly shifted base cases).
|
||||
|
||||
The key insight is recognising **overlapping subproblems**: calculating `ways(5)` requires `ways(4)` and `ways(3)`, but `ways(4)` also requires `ways(3)`. Without caching, we'd recalculate the same values exponentially many times.
|
||||
|
||||
approach: |
|
||||
We solve this using **Space-Optimised Dynamic Programming**:
|
||||
|
||||
**Step 1: Identify base cases**
|
||||
|
||||
- `ways(1) = 1`: Only one way to climb 1 step (take 1 step)
|
||||
- `ways(2) = 2`: Two ways to climb 2 steps (1+1 or 2)
|
||||
- If `n <= 2`, return `n` directly
|
||||
|
||||
|
||||
|
||||
**Step 2: Initialise tracking variables**
|
||||
|
||||
- `prev1 = 2`: Ways to reach step 2 (the "previous" step)
|
||||
- `prev2 = 1`: Ways to reach step 1 (the "step before previous")
|
||||
- We only need these two values — not the entire history!
|
||||
|
||||
|
||||
|
||||
**Step 3: Iterate from step 3 to n**
|
||||
|
||||
- For each step `i`, calculate `current = prev1 + prev2`
|
||||
- Shift the window: `prev2 = prev1`, `prev1 = current`
|
||||
- This simulates "sliding" up the staircase, only remembering the last two values
|
||||
|
||||
|
||||
|
||||
**Step 4: Return the result**
|
||||
|
||||
- After the loop, `prev1` contains `ways(n)`
|
||||
|
||||
|
||||
|
||||
This bottom-up approach avoids recursion overhead and uses only O(1) space by discarding values we no longer need.
|
||||
|
||||
common_pitfalls:
|
||||
- title: Using recursion without memoization
|
||||
- title: Naive Recursion Without Memoisation
|
||||
description: |
|
||||
Naive recursion recalculates the same subproblems repeatedly, leading to
|
||||
exponential time complexity. Either use memoization or iterative DP.
|
||||
wrong_approach: "return climb(n-1) + climb(n-2) without caching"
|
||||
correct_approach: "Use bottom-up DP or memoize recursive calls"
|
||||
The most intuitive approach — `return climb(n-1) + climb(n-2)` — has **exponential time complexity O(2^n)**.
|
||||
|
||||
- title: Off-by-one in base cases
|
||||
Without caching, we recalculate the same subproblems repeatedly. For example, `climb(5)` calls `climb(4)` and `climb(3)`. Then `climb(4)` calls `climb(3)` again!
|
||||
|
||||
The fix is either memoisation (top-down caching) or iterative DP (bottom-up).
|
||||
wrong_approach: "return climb(n-1) + climb(n-2) without caching"
|
||||
correct_approach: "Use @lru_cache decorator or iterative DP"
|
||||
|
||||
- title: Off-by-One in Base Cases
|
||||
description: |
|
||||
Carefully define base cases. There's 1 way to stay at ground (step 0),
|
||||
1 way to reach step 1, and 2 ways to reach step 2.
|
||||
Getting the base cases wrong leads to incorrect answers for all inputs. Remember:
|
||||
- `ways(0) = 1` (one way to stay at ground: do nothing)
|
||||
- `ways(1) = 1` (one way: take 1 step)
|
||||
- `ways(2) = 2` (two ways: 1+1 or 2)
|
||||
|
||||
Some solutions use `ways(0) = 1, ways(1) = 1` and start iteration from step 2. Others use `ways(1) = 1, ways(2) = 2` and start from step 3. Either works if consistent.
|
||||
wrong_approach: "Inconsistent base case definitions"
|
||||
correct_approach: "Define base cases clearly and iterate from the correct starting point"
|
||||
|
||||
- title: Stack Overflow on Large Inputs
|
||||
description: |
|
||||
Even with memoisation, recursive solutions can hit stack limits for large `n`. With `n <= 45`, this is usually fine in Python, but iterative solutions are safer and more efficient.
|
||||
|
||||
The iterative approach uses O(1) space and has no recursion overhead.
|
||||
wrong_approach: "Deep recursive calls without tail-call optimisation"
|
||||
correct_approach: "Use iterative bottom-up DP for guaranteed O(1) space"
|
||||
|
||||
key_takeaways:
|
||||
- Many counting problems follow Fibonacci-like patterns
|
||||
- Convert recursion to iteration for O(1) space
|
||||
- Bottom-up DP avoids stack overflow for large inputs
|
||||
- Recognize overlapping subproblems as a DP signal
|
||||
- "**Fibonacci pattern recognition**: Many counting problems follow this recurrence — recognise it instantly"
|
||||
- "**Space optimisation**: When you only need the last k values, don't store the entire DP array"
|
||||
- "**Bottom-up vs top-down**: Iterative DP avoids stack overhead and is often cleaner"
|
||||
- "**Foundation for harder DP**: This problem teaches the core DP concepts — optimal substructure and overlapping subproblems"
|
||||
|
||||
time_complexity: "O(n)"
|
||||
space_complexity: "O(1)"
|
||||
complexity_explanation: |
|
||||
Time: We compute n states, each in O(1).
|
||||
Space: Only track two previous values (space-optimized DP).
|
||||
time_complexity: "O(n). We compute each state from 3 to n exactly once, with O(1) work per state."
|
||||
space_complexity: "O(1). We only store two variables (`prev1` and `prev2`), regardless of input size."
|
||||
|
||||
solutions:
|
||||
- approach_name: Space-Optimized DP (Optimal)
|
||||
- approach_name: Space-Optimised DP
|
||||
is_optimal: true
|
||||
code: |
|
||||
def climb_stairs(n: int) -> int:
|
||||
# Base cases: 1 way for step 1, 2 ways for step 2
|
||||
if n <= 2:
|
||||
return n
|
||||
|
||||
prev1, prev2 = 2, 1
|
||||
# Track only the two previous values
|
||||
prev1 = 2 # ways(2)
|
||||
prev2 = 1 # ways(1)
|
||||
|
||||
# Build up from step 3 to n
|
||||
for i in range(3, n + 1):
|
||||
# Current = sum of two previous (Fibonacci recurrence)
|
||||
current = prev1 + prev2
|
||||
# Slide the window forward
|
||||
prev2 = prev1
|
||||
prev1 = current
|
||||
|
||||
return prev1
|
||||
explanation: |
|
||||
Track only the two previous values since that's all we need.
|
||||
Equivalent to computing the nth Fibonacci number.
|
||||
**Time Complexity:** O(n) — Single pass from 3 to n.
|
||||
|
||||
- approach_name: Recursive with Memoization
|
||||
**Space Complexity:** O(1) — Only two variables needed.
|
||||
|
||||
We recognise this as the Fibonacci sequence and compute it iteratively. By only tracking the two most recent values, we achieve optimal space complexity while avoiding recursion overhead.
|
||||
|
||||
- approach_name: Recursive with Memoisation
|
||||
is_optimal: false
|
||||
code: |
|
||||
from functools import lru_cache
|
||||
@@ -92,11 +168,16 @@ solutions:
|
||||
def climb_stairs(n: int) -> int:
|
||||
@lru_cache(maxsize=None)
|
||||
def dp(step: int) -> int:
|
||||
# Base cases
|
||||
if step <= 2:
|
||||
return step
|
||||
# Recurrence: ways to reach step = sum of two previous
|
||||
return dp(step - 1) + dp(step - 2)
|
||||
|
||||
return dp(n)
|
||||
explanation: |
|
||||
Top-down approach with memoization.
|
||||
Uses O(n) space for the cache and recursion stack.
|
||||
**Time Complexity:** O(n) — Each subproblem computed once due to caching.
|
||||
|
||||
**Space Complexity:** O(n) — Cache stores n values, plus recursion stack depth.
|
||||
|
||||
Top-down approach with memoisation. The `@lru_cache` decorator automatically caches results, preventing redundant calculations. While elegant, it uses more space than the iterative solution.
|
||||
|
||||
Reference in New Issue
Block a user