184 lines
7.0 KiB
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
|
|
|
|
|
|
|
|
**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: 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.
|