给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
分析:
1.这可以与走迷宫差不多,可以将所有的路径罗列出来,然后对比出最短的路径,在这里的话,可以将所有的开销可能都算出来,然后再对比出花费最小的。缺点是简单粗暴耗内存。
2.支付了花费后选择走一步还是选择走两步,不取决于接下来的那个台阶开销的高低,而应该取决于那个选择是走向整体开销最低的。这应该算是一个要注意的陷阱。
3.一时间想不出更艺术的想法,需要一点经验,就常规的话,这里面可能会重复计算很多次,应该会造成极大的内存浪费,可以结合昨天学到的动态规划、递归、记忆化这三个知识点减少内存的消耗。
我的代码:
class Solution(object):
def minCostClimbingStairs(self, cost, memo=None):
"""
:type cost: List[int]
:rtype: int
"""
if memo is None:
memo = {}
n = len(cost)
if n == 2:
return min(cost[0], cost[1])
if n in memo:
return memo[n]
# 计算到达第n阶的最低花费
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
self.minCostClimbingStairs(cost[:n-2], memo) + cost[n-2])
memo[n] = res
return res
报错了:
RuntimeError: maximum recursion depth exceeded
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
Line 18 in minCostClimbingStairs (Solution.py)
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
Line 18 in minCostClimbingStairs (Solution.py)
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
Line 18 in minCostClimbingStairs (Solution.py)
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
Line 18 in minCostClimbingStairs (Solution.py)
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
Line 18 in minCostClimbingStairs (Solution.py)
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
Line 18 in minCostClimbingStairs (Solution.py)
res = min(self.minCostClimbingStairs(cost[:n-1], memo) + cost[n-1],
........
.......
你的代码会触发 RuntimeError: maximum recursion depth exceeded
错误,这是因为你在递归时传递了 cost[:n-1]
和 cost[:n-2]
,导致每次递归调用都会创建新的子数组。由于每次递归都会生成新的 cost
数组,递归深度将很快增加,最终超出 Python 的最大递归深度限制。
问题所在:
- 每次递归调用时,你传递的是
cost[:n-1]
和cost[:n-2]
,这实际上会创建新的列表对象。因为每次递归传入的新列表长度都是n-1
或n-2
,导致递归深度急剧增加。
解决方案: 你应该避免在每次递归调用中切割列表,而是直接递归地处理当前索引 n
。同时,应该通过递减索引来计算花费,而不是创建新的子列表。
修改1:
class Solution(object):
def minCostClimbingStairs(self, cost, i=None, memo=None):
"""
:type cost: List[int]
:rtype: int
"""
if memo is None:
memo = {}
if i is None:
i = len(cost)
if i == 0:
return 0
if i == 1:
return 0
if i == 2:
return min(cost[0], cost[1])
if i in memo:
return memo[i]
# 计算到达第i阶的最低花费
res = min(self.minCostClimbingStairs(cost, i - 1, memo) + cost[i - 1],
self.minCostClimbingStairs(cost, i - 2, memo) + cost[i - 2])
memo[i] = res
return res
修改2:
class Solution(object):
def minCostClimbingStairs(self, cost):
"""
:type cost: List[int]
:rtype: int
"""
n = len(cost)
memo = {}
def minCost(i):
if i in memo:
return memo[i]
if i <= 1:
return 0
# 计算从第 i-1 或第 i-2 阶跳到第 i 阶的最小费用
res = min(minCost(i - 1) + cost[i - 1], minCost(i - 2) + cost[i - 2])
memo[i] = res
return res
# 从 `n` 这个点返回,表示到达顶部的最小费用
return minCost(n)
消耗内存最小的答案:
class Solution(object):
def minCostClimbingStairs(self, cost):
"""
:type cost: List[int]
:rtype: int
"""
length = len(cost)
if length == 0 or length == 1:
return 0
dp = [0]*(length+1) # dp[n] 到达下标为n的台阶所需要的最低话费
dp[0] = 0
dp[1] = 0
for i in range(2, len(cost)+1):
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
return dp[-1]
二者的区别、差距、可学习的地方(标准答案运行了4ms,而我的却是29ms,为什么?):
标准答案比你的代码运行更快,主要原因是它采用了动态规划(DP)的迭代方式,而你的代码使用的是递归 + 记忆化。以下是详细的分析:
1. 递归 vs 动态规划
-
递归 + 记忆化(你的方法)
递归的方式会不断地进行函数调用,每个函数调用都有一定的开销,尤其是在 Python 中。虽然你使用了记忆化来避免重复计算,但每次递归调用仍然需要处理函数调用栈和参数传递,这些都增加了运行时间。 -
动态规划(标准答案)
动态规划的迭代方式则避免了递归带来的函数调用开销。它直接使用一个数组dp
来存储每一步的最小花费,并通过循环一步步地计算出最终结果。由于所有的计算都在一个循环中进行,避免了递归的开销,因此速度更快。
2. 时间复杂度
-
你的代码的时间复杂度
虽然使用了记忆化,理论上时间复杂度仍然是 O(n),但由于递归函数调用的额外开销,导致实际运行时间较长。 -
标准答案的时间复杂度
标准答案的时间复杂度同样是 O(n),但因为使用的是迭代方式,省去了递归的开销,直接计算每一步的结果,所以运行速度更快。
3. 空间复杂度
-
你的代码的空间复杂度
你的代码使用递归调用,函数调用栈的深度可能达到O(n)
,这增加了额外的空间开销,尽管memo
减少了重复计算。 -
标准答案的空间复杂度
标准答案的空间复杂度是O(n)
,由于仅使用一个数组dp
来存储结果,并且没有递归调用栈的额外开销,因此更节省空间。
4. 代码执行效率
-
递归的函数调用栈
你的递归方法会涉及大量的函数调用栈操作,而函数调用栈操作是比较慢的。递归需要保存每一层的状态,Python 的递归深度有限制,且函数调用有一定的开销。 -
动态规划的顺序计算
标准答案通过顺序计算,避免了所有递归带来的开销。它通过线性遍历数组的方式,逐步构建dp
数组,从而以更高效的方式得出结果。
总结
标准答案的迭代方式更快的原因:
- 避免了递归的函数调用开销。
- 通过循环顺序计算,节省了内存和时间。
- 动态规划直接构建结果,时间和空间利用更高效。
从中可以学习到:
- 动态规划在处理类似问题时,经常比递归方法更高效,特别是在处理大型输入时。
- 减少递归调用的次数和深度,能显著提升代码运行效率。