Files
codetutor/backend/data/questions/climbing-stairs.yaml

184 lines
7.0 KiB
YAML

title: Climbing Stairs
slug: climbing-stairs
difficulty: easy
leetcode_id: 70
leetcode_url: https://leetcode.com/problems/climbing-stairs/
categories:
- dynamic-programming
- math
patterns:
- dynamic-programming
function_signature: "def climb_stairs(n: int) -> int:"
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`
examples:
- input: "n = 2"
output: "2"
explanation: "There are two ways: (1 step + 1 step) or (2 steps)."
- input: "n = 3"
output: "3"
explanation: "There are three ways: (1+1+1), (1+2), or (2+1)."
explanation:
intuition: |
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!
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
&nbsp;
**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!
&nbsp;
**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
&nbsp;
**Step 4: Return the result**
- After the loop, `prev1` contains `ways(n)`
&nbsp;
This bottom-up approach avoids recursion overhead and uses only O(1) space by discarding values we no longer need.
common_pitfalls:
- title: Naive Recursion Without Memoisation
description: |
The most intuitive approach — `return climb(n-1) + climb(n-2)` — has **exponential time complexity O(2^n)**.
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: |
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:
- "**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). 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-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
# 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: |
**Time Complexity:** O(n) — Single pass from 3 to n.
**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
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: |
**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.