理论基础
如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划的题型(常规)
- 基础问题(例如斐波那契数、爬楼梯)
- 背包问题
- 打家劫舍
- 股票问题
- 子序列问题
动态规划五部曲
- 确定 dp 数组以及下标的含义
- 真正的难点,想到一个合适的 dp 数组定义代表着对题目的正确理解,接下来的步骤就水到渠成了
- 确定递推公式
- dp 核心,但理解 dp 数组的含义之后基本就能得到正确的 dp 公式
- 要小心一些细节(例如 +1,-1,min,max)
- dp 数组的初始化
- 非常重要!初始化同样体现对于题目的理解。不同的初始化,dp 递推公式也会不同。确定了自己的思路后,再确定初始化。
- 初始化非常多样,全是 0,全是 1,开头是 0,开头是1其余全是0,都有可能
- 最后一步举例推导,常常能捕捉到错误的初始化
- 确定遍历顺序
- 对于二维数组来说,遍历顺序尤其重要
- for loop 的嵌套顺序翻转,for loop 内的递推公式顺序,可能导致错误,也可能不产生影响。只有理解了每一种遍历顺序的原因,才能判断应该用哪一种顺序。
- 举例推导 dp 数组
- dp 数组的最好的 debug 工具就是将 dp 数组直接打印出来,进行手推
- 如果和自己的思路推演不一样,就确定是自己的代码实现出了问题
- 如果和自己的思路推演一样,说明 dp 的前四步出错了
509. 斐波那契数
- dp 数组下标的含义:
dp[i]是第 i+1 个斐波那契数 - dp 递推公式:
dp[i] = dp[i-1] + dp[i-2] - dp 数组的初始化:
dp[0] = 0, dp[1] = 1 - 遍历顺序:根据递推,简单的从小到大
- 举例推导 dp 数组:省略
本题是非常基础的 dp,主要是熟悉 dp 五部曲的方法论。作为基础的 dp,维护整个 dp 数组有些昂贵了,只需要维护前两个值即可。
class Solution:
def fib(self, n: int) -> int:
if n == 0:
return 0
if n == 1:
return 1
# initialization
dp = [0] * (n+1)
dp[1] = 1
# dp formula
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[-1]
70. 爬楼梯
- dp 数组下标的含义:
dp[i]是到达第 i 层的方法总数 - dp 递推公式:
dp[i] = dp[i-1] + dp[i-2],因为到达第 i 层的方法有两种- 到达第 i - 2 层,然后一步走两阶,到达第 i 层
- 到达第 i - 1 层,然后一步走一阶,到达第 i 层
- dp 数组的初始化:
dp[0] = 1,其余均为 0- 由于起始位置就是“不存在的”第 0 层,所以初始化为 1,其余位置初始化为 0
- 但更好的解释是,本题的输入是
n
≥
1
n\geq1
n≥1,所以根本没必要考虑
dp[0]的初始化,而是应该直接选择初始化dp[1] = 1, dp[2] = 2,然后根据递推公式计算。
- 遍历顺序:根据递推,简单的从小到大
- 举例推导 dp 数组:
idx = [0, 1, 2, 3, 4, 5] dps = [ , 1, 2, 3, 5, 8]
class Solution:
def climbStairs(self, n: int) -> int:
# dp[i] represents the number of ways to reach position i
dp = [0] * (n+1)
dp[0] = 1
# dp formula
for i in range(1, n+1):
if i - 1 >= 0:
dp[i] += dp[i-1]
if i - 2 >= 0:
dp[i] += dp[i-2]
return dp[-1]
拓展
本题中,如果定义 dp[0] = 1,就可以发难了:为什么 dp[0] 一定要初始化为1,此时可能候选人就要强行给 dp[0] 应该是1找各种理由。那这就是一个考察点了,对 dp[i] 的定义理解的不深入。
另外,本题一个变种就是如果每一步能够爬 m 阶(本题 m=2),那么有多少种解法。区别只是在每一层内部加一个 for loop 循环。
746. 使用最小花费爬楼梯
本题的题目描述有些令人费解。个人来说,cost[i] 可以理解为离开第 i 层(向上爬 1 或 2 阶)所需要的代价。
- dp 数组下标的含义:
dp[i]是到达第 i 层的最少代价 - dp 递推公式:
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]),因为到达第 i 层的方法有两种- 花费
dp[i-2]到达第 i - 2 层,然后花费cost[i-2]一步走两阶,到达第 i 层 - 花费
dp[i-1]到达第 i - 1 层,然后花费cost[i-1]一步走一阶,到达第 i 层 - 选取这两种方法中花费更少的即可得到
dp[i]
- 花费
- dp 数组的初始化:
dp[0] = 1, dp[1] = 0,其余均为 0(其余位置会递推得到)- 题目允许直接从第 0 层或第 1 层开始,所以到达这两层的代价均为 0。
- 遍历顺序:根据递推,简单的从小到大
- 举例推导 dp 数组:
cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] dps = [0, 0, 1, 2, 2, 3, 3, 4, 4, 5, 6]
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
# dp[i] represents the min cost to reach index i
dp = [0] * (len(cost) + 1)
dp[0] = 0
dp[1] = 0
# dp formula
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]

被折叠的 条评论
为什么被折叠?



