题目
70. 爬楼梯
难度:简单
题目分析:这道题是动态规划系列的第一个例题,因此,这里总结下使用动态规划的三部曲。
1. 定义数组 dp[n] 的含义,这个根据具体题目来赋予含义, 这道题是 台阶 n 拥有多少种走法
2. 找出更新数组的递归公式,比如这题是
d
p
[
n
]
=
d
p
[
n
−
1
]
+
d
p
[
n
−
2
]
dp[n] = dp[n-1]+dp[n-2]
dp[n]=dp[n−1]+dp[n−2], 这里含义是,台阶 n 可以是在 台阶 n-1 迈一步到达,也可以从台阶 n-2 迈两步到达。动态规划的含义就是充分利用之前的数据, 于是不难明白,dp[n-1] 和 dp[n-2]在计算dp[n]之前,取值必须刷新
3. 设置初始条件, 因为数组dp索引的含义是多少级楼梯,所以, n 要大于等于0。 当
n
=
0
,
1
,
n =0, 1,
n=0,1,时,没法使用递推公式更新,因此呢,我们自己手算填入。
以上便是使用动态规划需要的三部曲。关键点是头两步,定义好数组代表的含义,以及找出递推公式,难点也正是这里。
适合使用动态规划的题目,一般都是问,最大值、最小值可以是多少,某某有多少种解法,这类题目,因此也可以使用基于队列的广度优先搜索(BFS)去做。使用动态规划,无法给出具体的操作,它只能告诉我们答案有多少。如果题目还需要输出具体步骤,那只能借助于BFS或DFS来求解。
1. 解法一:动态规划(正统解法)
class Solution:
def climbStairs(self, n: int) -> int:
if n < 2:
return 1
dp =[0]*(n+1) # 表示n个台阶有几种解法
# 初始条件
dp[1], dp[0]= 1, 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
1.1 运行结果
1.2 分析:
很奇怪,时间很慢。这里时间复杂度是 O(n), 空间复杂度也是O(n)。
考虑到每一个数字,最多使用两次,所以存储空间可以节省。 下面是使用O(1)空间复杂度的解法。
2. 解法二: 动态规划改进版
class Solution:
def climbStairs(self, n: int) -> int:
# 使用O(1)的空间
# 递推公式其实就是斐波那契数列
if n < 2:
return 1
f0, f1 = 1, 1
for i in range(2, n+1):
f = f0 + f1
f1, f0 = f, f1
return f1
2.1 运行结果:
2.2 分析:
这个算法时间复杂度依然是 O(n)。
解法三:递归
class Solution:
def climbStairs(self, n: int) -> int:
# 自然可以使用递归
def fn(n):
if n < 2:
return 1
return fn(n-1) + fn(n-2)
return fn(n)
3.1 运行结果
3.2 分析:
斐波那契数列的爆炸式增长,大家是知道的, 输入是38,输出就是千万量级。
另外,单纯使用递归的话,嵌套层数会非常深,里面会有大量重复计算,因此,又是不能免俗的引进额外空间来辅助存储。其实也就回到了动态规划的解法。
3.3 改进:额外存储空间
f_array = [0]*(n+1)
def fn(n,f_array):
if n < 2:
return 1
if f_array[n]!= 0:
return f_array[n]
f_array[n] = fn(n-1, f_array) + fn(n-2, f_array)
return f_array[n]
return fn(n, f_array)
3.3.1 运行结果:
3.3.2 分析:
改进后代码,其实跟动态规划差别已经不大了,所以,这类题目,用递归的效益不高啊。
总结:
动态规划或许可以粗略的理解为带中间信息存储的递归算法,因为他们二者都充分利用规模更小问题的答案, 只是动态规划是在数组中递推,而不是函数调用。
上面的问题,其实就是斐波那契数列的求解,在数学上,还有时间复杂度是 O(log n)的解法,是最快的!不过由于不是本篇的重点,暂时不做讨论,有兴趣的可以点击以下链接到Leetcode官网看官方解释。