I was solving leetcode climbing stairs problem:
You are given an integer array cost where
cost[i]
is the cost of ith step on a staircase. Once you pay the cost, you can either climb one or two steps. You can either start from the step with index0
, or the step with index1
. Return the minimum cost to reach the top of the floor.
I solved it using Top Down dynamic programming approach with memoization using python @cache
as follows:
from functools import cache
class Solution:
def minCostClimbingStairs(self, cost) -> int:
@cache
def minCostAux(curStep):
if curStep == -1: return 0
if curStep == 0: return cost[0]
return min(minCostAux(curStep-2) + cost[curStep], minCostAux(curStep-1) + cost[curStep])
cost.append(0)
return minCostAux(len(cost)-1)
I love how using @cache
makes the solution look minimal.
The bottom up recursive solution without @cache
looks like this:
class Solution:
def minCostClimbingStairs(self, cost) -> int:
def minCostAux(step_i):
# ===== return memoized case =====
if minCosts[step_i] != -1:
return minCosts[step_i]
# ===== memoize =====
minCosts[step_i] = min(minCostAux(step_i-1 )
, minCostAux(step_i-2)) + cost[step_i]
# ===== return base CEIL case =====
if step_i == len(cost)-1: return minCosts[step_i]
# ===== recurse bottom up =====
return minCostAux(step_i+1)
cost.append(0) # this result in one extra step to get final answer (check Note above)
# ===== initialize memory =====
minCosts = [-1] * len(cost)
# ===== memoize base cases =====
minCosts[0] = cost[0]
minCosts[1] = min(cost[0]+cost[1], cost[1])
# ===== recurse bottom up =====
return minCostAux(2)
I was curious if we can implement bottom up recursive solution using @cache
. It could have looked something like this:
from functools import cache
class Solution:
def minCostClimbingStairs(cost) -> int:
@cache
def minCostAux(step_i):
if step_i == len(cost)-1: return minCosts[step_i]
minCosts[step_i] = min(minCostAux(step_i-1)
, minCostAux(step_i-2, cost)) + cost[step_i] # ***
return minCostAux(step_i+1) # ***
cost.append(0)
return minCostAux(2, cost)
This doesnt work as I am not able to figure out how can I appropriately combine lines commented as # ***
. In top down, we memoize by returning current problem solution which is calculated by recursing subproblems. But, in bottom up we also have to recurse for bigger problem: +1
in minCostAux(step_i+1)
. Is this doable with @cache
? Am just curious if there is no way to do it!
PS 1
I know we can do it by explicitly maintaining cache dictionary, but that again adds some code lines. But, I want to look it as minimal as possible.
PS 2
Sample runs for testing:
print(Solution().minCostClimbingStairs([10,15,20])) # 15
print(Solution().minCostClimbingStairs([1,100,1,1,1,100,1,1,100,1])) # 6
A bottom up recursive solution using cache
from functools import cache # cache only available in Python 3.9+, earlier use lru_cache
class Solution(object):
def minCostClimbingStairs(self, cost):
"""
:type cost: List[int]
:rtype: int
"""
def get_val(index):
' get value from cost array (handling index limit) '
try:
return cost[index]
except IndexError:
return 0
@cache # note: same as lru_cache(maxsize=None) if using lru_cache
def minCostAux(curStep):
if curStep >= len(cost):
return 0 # Pass limit of cost array
return min(
minCostAux(curStep + 1) + get_val(curStep + 1), # min forward of step by 1
minCostAux(curStep + 2) + get_val(curStep + 2)) # min forward of step by 2
return minCostAux(-1)
print(Solution().minCostClimbingStairs([10, 15, 20])) # Output: 15
print(Solution().minCostClimbingStairs([1,100,1,1,1,100,1,1,100,1])) # Output: 6
Performance
cost = list(range(400, 0, -1))
%timeit Solution().minCostClimbingStairs(cost)
568 µs ± 8.29 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)