Problem使用最小花费爬楼梯
解题方法1
思路
最简单的办法就是用递归
问题定义,假设要爬n级台阶,即数组长度为n,那么这n级台阶要么是从第n级台阶跨越到顶端,要么是第n-1级
比较这两个大小即可
代码
Solution1:
# 采用递归的形式
# 思路是将n级台阶问题转化为n-1级台阶与n-2级台阶问题比较
# 原始问题是,只有两级台阶要跨一步,只有一级台阶则不用跨步
def minCostClimbingStairs(self, cost):
"""
:type cost: List[int]
:rtype: int
"""
return self.climb(cost, len(cost))
def climb(self, cost, idx):
if idx == 1:
return 0
if idx == 2:
return cost[0] if cost[0] < cost[1] else cost[1]
return cost[idx - 1] + self.climb(cost, idx - 1) \
if cost[idx - 1] + self.climb(cost, idx - 1) < cost[idx - 2] + self.climb(cost, idx - 2) \
else cost[idx - 2] + self.climb(cost, idx - 2)
复杂度
对于每一级台阶 n,递归都会分为两个子问题,每个子问题又继续分裂成两个更小的子问题,类似于二叉树的增长。这种递归的时间复杂度是指数级的,具体来说是 O ( 2 n ) O(2^n) O(2n)。这意味着随着 n(台阶数)的增加,所需的计算量会非常快地增加。
空间复杂度就是栈的调用, O ( n ) O(n) O(n)
解题方法2
思路
思路1中的递归存在大量的重复递归调用,n级台阶问题需要调用n-1级台阶问题和n-2级台阶问题,这些子问题又要调用对应的子问题,子问题之间有重叠,因此加上一个备忘录即可
代码
class Solution2:
# 思路是在暴力递归的基础上让这个问题带一个备忘录
table = {}
def minCostClimbingStairs(self, cost):
"""
:type cost: List[int]
:rtype: int
"""
return self.climb(cost, len(cost))
def climb(self, cost, idx):
if idx == 1:
return 0
if idx == 2:
return cost[0] if cost[0] < cost[1] else cost[1]
if idx in self.table:
return self.table[idx]
else:
res = cost[idx - 1] + self.climb(cost, idx - 1) \
if cost[idx - 1] + self.climb(cost, idx - 1) < cost[idx - 2] + self.climb(cost, idx - 2) \
else cost[idx - 2] + self.climb(cost, idx - 2)
self.table[idx] = res
return res
复杂度
备忘录法确保每个台阶只计算一次,之后直接使用缓存的结果。因此,对于每个台阶 idx,计算一次之后就将结果保存,后续的递归调用直接从 table 字典中取得结果,避免了重复计算。
由于每个台阶只被计算一次,且每次计算的时间都是常数时间(假设字典操作是常数时间),因此时间复杂度降低为 O(n),其中 n 是台阶数。
空间复杂度:主要由两部分组成:
递归栈:在最坏情况下,递归的深度为 n,因此递归栈的空间复杂度为 O(n)。
备忘录存储:备忘录 table 用于存储每级台阶的计算结果,最多存储 n 个结果,因此空间复杂度也为 O(n)。
解题方法3
思路
递归是自上而下的问题解决方式,一个问题的解决需要其子问题的支撑,所以想到如果能先计算它的子问题,就可以得到问题的解,也就是自下而上的解决方式
错误思路
首先想到要爬n个台阶时,要么从第n个台阶爬上去,此时的花费是爬到第n个台阶所用的最小花费和第n个台阶的花费;要么从第n-1个台阶爬上去,此时的花费是爬到第n-1个台阶所用的最小花费和第n-1个台阶的花费,所以问题规模为n可以通过问题规模为n-1以及问题规模为n-2解决;这里的问题规模n指的是数组的长度,即台阶的数量;
这个思路为什么错:
规模为n的问题,不是建立在规模为n-1和规模为n-2的基础之上,举个例子,[10,15,30];
这三台阶最小话费是15;
但是当问题规模为1时,[10]最小花费是10
当问题规模为2时,[10,15]最小花费还是10
当问题规模为3时,建立在问题规模为1和2时的最小花费就是10+30 = 40,这显然是不对的
因为问题规模为3时,从15走最好,这利用了问题规模为2的情况但是15并不是问题规模为2的最优解,换句话说,它的子问题最优解没有到15这个台阶的;
正确思路
按照上面的例子,当台阶数为3时,要么从第2个台阶爬上来,要么从第1个台阶爬上来,换句话说,要知道爬到第2个台阶的最小花费和爬到第一个第一个台阶的最小花费;
因此要得到n个台阶的最小花费,要先得到爬到第n-1个台阶的花费和爬到第n-2个台阶的最小花费
以[10,15,20,25]为例子:
只有一个台阶[10]时,最小花费就是它自己10;
两个台阶[10,15]时,最小花费就是15
三个台阶[10,15,20]时,最小花费明显是从第一个台阶爬过来10+20=30
四个台阶[10,15,20,25]时,此时前三个台阶的最小花费数组[10,15,30],要么从第二个台阶跨到第四个台阶,要么从第三个台阶跨到第四个台阶,显然从第二个跨花费小,代价就是15+25=40,那么前四个台阶的花费就是[10,15,30,40]接下来的到第五、第六个台阶的花费就很自然;
思路就是填充这个最小花费数组res,填充的规则,第一个和第二个数组都是可以直接填充进去,从第三个开始
res[i] = cost[i] + min{res[i-1],res[i-2]}
最后检查res数组中倒数两个比较小的值就可以了
代码
def minCostClimbingStairs(self, cost):
if len(cost) == 1:
return 0
if len(cost) == 2:
return cost[0] if cost[0] < cost[1] else cost[1]
res = [cost[0],cost[1]]
for i in range(2, len(cost)):
min = res[i-1] if res[i-1] < res[i-2] else res[i-2]
res.append(min + cost[i])
return res[-1] if res[-2] > res[-1] else res[-2]
复杂度
时间复杂度一看就是
O
(
n
)
O(n)
O(n)
空间复杂度用了一个列表也是
O
(
n
)
O(n)
O(n)