day3 使用最小花费爬楼梯

给你一个整数数组 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-1n-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 数组,从而以更高效的方式得出结果。

总结

标准答案的迭代方式更快的原因

  • 避免了递归的函数调用开销。
  • 通过循环顺序计算,节省了内存和时间。
  • 动态规划直接构建结果,时间和空间利用更高效。

从中可以学习到

  • 动态规划在处理类似问题时,经常比递归方法更高效,特别是在处理大型输入时。
  • 减少递归调用的次数和深度,能显著提升代码运行效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值