746. Min Cost Climbing Stairs
Problem Description
You have a staircase where each step has a cost associated with it, represented by an integer array cost
. The value cost[i]
represents the cost of stepping on the i
-th step.
The rules for climbing the staircase are:
- After paying the cost of a particular step, you can climb either one step or two steps from that position
- You can choose to start your climb from either step
0
or step1
(you don't need to pay to stand on the starting step) - The "top" of the floor is considered to be just beyond the last step in the array
Your goal is to find the minimum total cost needed to reach the top of the staircase.
For example, if cost = [10, 15, 20]
:
- You could start at step
0
, pay10
, climb 2 steps to step2
, pay20
, then climb 1 or 2 steps to reach the top. Total cost:10 + 20 = 30
- Or you could start at step
1
, pay15
, climb 2 steps directly to the top. Total cost:15
- The minimum cost would be
15
The solution uses dynamic programming with memoization. The function dfs(i)
calculates the minimum cost to reach the top starting from step i
. Since we can start from either step 0
or step 1
, the final answer is the minimum of dfs(0)
and dfs(1)
.
Intuition
The key insight is that at each step, we face a decision: pay the current cost and either climb 1 step or climb 2 steps. This creates a branching decision tree where we want to find the path with minimum total cost.
Think of it backwards - to reach the top from any position i
, we must:
- Pay the cost at position
i
(which iscost[i]
) - Then make the optimal choice between climbing 1 or 2 steps
This naturally leads to a recursive structure. If we define dfs(i)
as "the minimum cost to reach the top starting from step i
", then:
dfs(i) = cost[i] + min(dfs(i+1), dfs(i+2))
The base case occurs when i >= len(cost)
, meaning we've already reached or passed the top, so no additional cost is needed (return 0
).
Why does this work? Because we're breaking down the problem into smaller subproblems. The minimum cost from step i
depends only on the minimum costs from steps i+1
and i+2
. By solving these smaller problems first (through recursion) and combining their results, we build up to the solution for the original problem.
The memoization (@cache
) is crucial here because the same positions will be visited multiple times through different paths. For instance, we might reach step 3 by climbing from step 1 (with a 2-step climb) or from step 2 (with a 1-step climb). Without memoization, we'd recalculate the minimum cost from step 3 multiple times, leading to exponential time complexity.
Since we can start from either step 0
or step 1
, we compute both dfs(0)
and dfs(1)
and take the minimum as our final answer.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach using memoization search. Here's how the implementation works:
Core Function Design:
We define a recursive function dfs(i)
that calculates the minimum cost to reach the top starting from the i
-th step. This function follows the recurrence relation:
dfs(i) = cost[i] + min(dfs(i + 1), dfs(i + 2))
Base Case:
When i >= len(cost)
, we've reached or passed the top of the staircase. Since no further climbing is needed, we return 0
.
Recursive Case:
For any valid step i
:
- We must pay
cost[i]
to use this step - We then choose the cheaper option between:
- Climbing 1 step:
dfs(i + 1)
- Climbing 2 steps:
dfs(i + 2)
- Climbing 1 step:
- Return the sum:
cost[i] + min(dfs(i + 1), dfs(i + 2))
Memoization with @cache:
The @cache
decorator automatically stores the results of dfs(i)
for each unique value of i
. When the function is called again with the same parameter, it returns the cached result instead of recalculating. This optimization reduces the time complexity from O(2^n) to O(n).
Finding the Final Answer:
Since we can start from either step 0
or step 1
, we compute:
dfs(0)
: minimum cost starting from step 0dfs(1)
: minimum cost starting from step 1
The final answer is min(dfs(0), dfs(1))
.
Example Walkthrough:
For cost = [10, 15, 20]
:
dfs(3)
returns0
(base case - beyond the top)dfs(2)
returns20 + min(dfs(3), dfs(4))
=20 + min(0, 0)
=20
dfs(1)
returns15 + min(dfs(2), dfs(3))
=15 + min(20, 0)
=15
dfs(0)
returns10 + min(dfs(1), dfs(2))
=10 + min(15, 20)
=25
- Final answer:
min(25, 15)
=15
The memoization ensures each state is computed only once, making the solution efficient with O(n) time and space complexity.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
.
We'll trace through the recursive calls to understand how the solution finds the minimum cost.
Step 1: Start the computation
We need to find min(dfs(0), dfs(1))
.
Step 2: Calculate dfs(0)
dfs(0) = cost[0] + min(dfs(1), dfs(2))
dfs(0) = 1 + min(dfs(1), dfs(2))
- We need to compute
dfs(1)
anddfs(2)
Step 3: Calculate dfs(1)
dfs(1) = cost[1] + min(dfs(2), dfs(3))
dfs(1) = 100 + min(dfs(2), dfs(3))
- We need
dfs(2)
anddfs(3)
Step 4: Build up from the end Let's trace backwards from the base cases:
dfs(10) = 0
(base case, we're at the top)dfs(9) = cost[9] + min(dfs(10), dfs(11)) = 1 + min(0, 0) = 1
dfs(8) = cost[8] + min(dfs(9), dfs(10)) = 100 + min(1, 0) = 100
dfs(7) = cost[7] + min(dfs(8), dfs(9)) = 1 + min(100, 1) = 2
dfs(6) = cost[6] + min(dfs(7), dfs(8)) = 1 + min(2, 100) = 3
dfs(5) = cost[5] + min(dfs(6), dfs(7)) = 100 + min(3, 2) = 102
dfs(4) = cost[4] + min(dfs(5), dfs(6)) = 1 + min(102, 3) = 4
dfs(3) = cost[3] + min(dfs(4), dfs(5)) = 1 + min(4, 102) = 5
dfs(2) = cost[2] + min(dfs(3), dfs(4)) = 1 + min(5, 4) = 5
Step 5: Complete the calculation
Now we can finish calculating dfs(1)
and dfs(0)
:
dfs(1) = 100 + min(dfs(2), dfs(3)) = 100 + min(5, 5) = 105
dfs(0) = 1 + min(dfs(1), dfs(2)) = 1 + min(105, 5) = 6
Step 6: Find the minimum starting point
- Starting from step 0:
dfs(0) = 6
- Starting from step 1:
dfs(1) = 105
- Minimum cost = min(6, 105) = 6
The optimal path: Start at step 0 (pay 1) → jump to step 2 (pay 1) → jump to step 4 (pay 1) → jump to step 6 (pay 1) → jump to step 7 (pay 1) → jump to step 9 (pay 1) → reach the top. Total cost: 6.
Notice how memoization helps here - dfs(2)
is needed for both dfs(0)
and dfs(1)
, but it's only calculated once and then cached for reuse.
Solution Implementation
1from typing import List
2from functools import cache
3
4
5class Solution:
6 def minCostClimbingStairs(self, cost: List[int]) -> int:
7 """
8 Find the minimum cost to reach the top of the staircase.
9 You can start from either step 0 or step 1, and from each step
10 you can climb either 1 or 2 steps.
11
12 Args:
13 cost: List where cost[i] is the cost of stepping on the ith step
14
15 Returns:
16 Minimum cost to reach the top (beyond the last step)
17 """
18
19 @cache
20 def dfs(current_step: int) -> int:
21 """
22 Calculate minimum cost starting from current_step to reach the top.
23
24 Args:
25 current_step: Current position on the staircase
26
27 Returns:
28 Minimum cost from current_step to the top
29 """
30 # Base case: if we've reached or passed the top, no additional cost
31 if current_step >= len(cost):
32 return 0
33
34 # Pay the cost of current step and choose minimum between:
35 # - Taking 1 step forward
36 # - Taking 2 steps forward
37 one_step = dfs(current_step + 1)
38 two_steps = dfs(current_step + 2)
39
40 return cost[current_step] + min(one_step, two_steps)
41
42 # Can start from either step 0 or step 1, return the minimum
43 start_from_zero = dfs(0)
44 start_from_one = dfs(1)
45
46 return min(start_from_zero, start_from_one)
47
1class Solution {
2 // Memoization array to store computed results for each stair index
3 private Integer[] memo;
4 // Reference to the input cost array
5 private int[] cost;
6
7 /**
8 * Finds the minimum cost to reach the top of the staircase.
9 * You can start from either step 0 or step 1.
10 *
11 * @param cost Array where cost[i] is the cost of stepping on the i-th stair
12 * @return The minimum cost to reach the top
13 */
14 public int minCostClimbingStairs(int[] cost) {
15 this.cost = cost;
16 this.memo = new Integer[cost.length];
17
18 // Try starting from both step 0 and step 1, return the minimum
19 return Math.min(dfs(0), dfs(1));
20 }
21
22 /**
23 * Recursive helper function with memoization to calculate minimum cost
24 * from a given stair index to the top.
25 *
26 * @param currentIndex The current stair position
27 * @return The minimum cost to reach the top from currentIndex
28 */
29 private int dfs(int currentIndex) {
30 // Base case: if we've reached or passed the top, no additional cost
31 if (currentIndex >= cost.length) {
32 return 0;
33 }
34
35 // Check if we've already computed the result for this index
36 if (memo[currentIndex] == null) {
37 // Calculate minimum cost: current stair cost + minimum of next two possible steps
38 int oneStep = dfs(currentIndex + 1);
39 int twoSteps = dfs(currentIndex + 2);
40 memo[currentIndex] = cost[currentIndex] + Math.min(oneStep, twoSteps);
41 }
42
43 return memo[currentIndex];
44 }
45}
46
1class Solution {
2public:
3 int minCostClimbingStairs(vector<int>& cost) {
4 int n = cost.size();
5
6 // dp[i] represents the minimum cost to reach the top starting from step i
7 // Initialize with -1 to indicate uncomputed state
8 vector<int> dp(n, -1);
9
10 // Recursive function with memoization to calculate minimum cost
11 // Lambda function using explicit object parameter (C++23 feature)
12 auto dfs = [&](this auto&& dfs, int currentStep) -> int {
13 // Base case: if we've reached or passed the top, no cost needed
14 if (currentStep >= n) {
15 return 0;
16 }
17
18 // If already computed, return the memoized result
19 if (dp[currentStep] != -1) {
20 return dp[currentStep];
21 }
22
23 // Calculate minimum cost: current step cost + minimum of next two steps
24 // We can either take 1 step or 2 steps from current position
25 dp[currentStep] = cost[currentStep] +
26 min(dfs(currentStep + 1), dfs(currentStep + 2));
27
28 return dp[currentStep];
29 };
30
31 // We can start from either step 0 or step 1
32 // Return the minimum cost between these two starting points
33 return min(dfs(0), dfs(1));
34 }
35};
36
1/**
2 * Calculates the minimum cost to climb stairs where you can climb 1 or 2 steps at a time.
3 * You can start from either step 0 or step 1.
4 *
5 * @param cost - Array where cost[i] represents the cost of stepping on the i-th stair
6 * @returns The minimum cost to reach the top of the stairs
7 */
8function minCostClimbingStairs(cost: number[]): number {
9 const stairsCount: number = cost.length;
10 // Memoization array to store computed minimum costs for each position
11 // -1 indicates the value hasn't been computed yet
12 const memo: number[] = Array(stairsCount).fill(-1);
13
14 /**
15 * Recursive helper function to calculate minimum cost from position i to the top.
16 * Uses dynamic programming with memoization to avoid redundant calculations.
17 *
18 * @param position - Current position on the stairs
19 * @returns Minimum cost to reach the top from current position
20 */
21 const calculateMinCost = (position: number): number => {
22 // Base case: if we've reached or passed the top, no additional cost
23 if (position >= stairsCount) {
24 return 0;
25 }
26
27 // If we haven't computed the minimum cost for this position yet
28 if (memo[position] < 0) {
29 // Cost at current position plus minimum of:
30 // - Taking 1 step forward
31 // - Taking 2 steps forward
32 memo[position] = cost[position] + Math.min(
33 calculateMinCost(position + 1),
34 calculateMinCost(position + 2)
35 );
36 }
37
38 return memo[position];
39 };
40
41 // Return minimum cost starting from either step 0 or step 1
42 return Math.min(calculateMinCost(0), calculateMinCost(1));
43}
44
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array cost
. This is because the recursive function dfs(i)
with memoization (@cache
) ensures that each index from 0 to n-1
is computed at most once. Without memoization, it would be O(2^n)
due to the overlapping subproblems, but the @cache
decorator stores previously computed results, reducing the number of unique function calls to n
.
The space complexity is O(n)
, where n
is the length of the array cost
. This consists of two components:
- The recursion call stack depth, which can go up to
O(n)
in the worst case when we traverse from index 0 ton
by taking single steps. - The memoization cache, which stores at most
n
different results (one for each possible index value from 0 ton-1
).
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Problem's End Condition
The Mistake:
Many people incorrectly assume they need to land exactly on the last step (index n-1
). This leads to wrong implementations like:
# INCORRECT - assumes we must land on the last step
@cache
def dfs(i):
if i == len(cost) - 1: # Wrong base case!
return cost[i]
if i >= len(cost):
return float('inf') # Penalizing overshooting
return cost[i] + min(dfs(i + 1), dfs(i + 2))
Why It's Wrong: The problem states the top is "beyond the last step." You can reach the top by either:
- Landing on the last step and taking 1 more step
- Landing on the second-to-last step and taking 2 steps
The Fix: Use the correct base case that returns 0 when you've reached or passed the array bounds:
if i >= len(cost):
return 0 # Successfully reached the top
Pitfall 2: Forgetting to Consider Both Starting Points
The Mistake: Only considering starting from step 0:
# INCORRECT - only considers starting from step 0
def minCostClimbingStairs(self, cost):
@cache
def dfs(i):
if i >= len(cost):
return 0
return cost[i] + min(dfs(i + 1), dfs(i + 2))
return dfs(0) # Missing dfs(1)!
Why It's Wrong: The problem explicitly allows starting from either step 0 or step 1 without paying their cost initially. You only pay when you step ON a stair to climb from it.
The Fix: Always check both starting positions and take the minimum:
return min(dfs(0), dfs(1))
Pitfall 3: Incorrect Handling of Small Arrays
The Mistake: Not handling edge cases where the array has only 1 or 2 elements:
# Potentially problematic without proper base case cost = [10] # Can start at step 0, pay 10, and reach the top cost = [10, 15] # Can start at step 1, pay 15, and reach the top
Why It Could Be Wrong:
Without the proper base case (if i >= len(cost): return 0
), the recursion might not handle these cases correctly.
The Fix:
The base case if i >= len(cost): return 0
naturally handles all array sizes:
- For
cost = [10]
:dfs(0) = 10 + min(dfs(1), dfs(2))
where bothdfs(1)
anddfs(2)
return 0 - For
cost = [10, 15]
: Works similarly with proper base case handling
Pitfall 4: Not Using Memoization
The Mistake: Implementing the recursive solution without memoization:
# INCORRECT - exponential time complexity without memoization
def dfs(i):
if i >= len(cost):
return 0
return cost[i] + min(dfs(i + 1), dfs(i + 2))
Why It's Wrong:
Without memoization, the same subproblems are calculated multiple times, leading to O(2^n) time complexity. For example, dfs(2)
might be calculated from both dfs(0)
and dfs(1)
.
The Fix: Always use memoization through either:
@cache
decorator (simplest)@lru_cache(None)
- Manual dictionary for storing results
The memoization reduces time complexity from O(2^n) to O(n).
Which of these properties could exist for a graph but not a tree?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!