本篇文章是动态规划解题套路框架 :: labuladong的算法小抄的笔记。
动态规划
动态规划目的:求最值
动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离等等。
动态规划核心问题:穷举
因为要求最值,所以把所有可行的答案穷举出来,然后在其中找最值。
动态规划三要素
-
只有列出正确的「状态转移方程」,才能正确地穷举;
-
需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值;
-
动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素
动态规划难点:写出状态转移方程
通过思维框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义dp数组/函数的含义,辅助思考状态转移方程。
按上面的套路,最后的解法代码就会是如下的框架:
# 自顶向下递归的动态规划
def dp(状态1, 状态2, ...):
for 选择 in 所有可能的选择:
# 此时的状态已经因为做了选择而改变
result = 求最值(result, dp(状态1, 状态2, ...))
return result
# 自底向上迭代的动态规划
# 初始化
base case dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
练习题
题目一:斐波那契数 - 力扣(LeetCode)
斐波那契数列虽然简单,但可以很好的帮助我们理解动态规划
题目描述:
暴力解法:穷举法。从头到尾依次计算。
穷举法
class Solution:
def fib(self, n: int) -> int:
if n == 0 or n == 1:
return n
return self.fib(n-1) + self.fib(n-2)
写出穷举法就是成功的第一步,接下来就是考虑如何进行优化。
优化一:通过带备忘录的递归方法,避免重复的计算
带备忘录的递归方法
class Solution:
def fib(self, n: int) -> int:
memo = dict() # 开启备忘录 其实创建了字典用来存会重复计算的结果
def my_fib(x):
if x < 2:
return x
if x in memo:
return memo[x]
res = my_fib(x-1) + my_fib(x-2)
# 将其结果计入备忘录,下次遇到相同的 直接从字典里进行返回
memo[x] = res
return memo[x]
return my_fib(n)
有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,通常叫做 DP table
dp 数组的迭代(递推)解法
class Solution:
def fib(self, n: int) -> int:
self.dp=[-1] * (n+1)
if n == 0:
return 0
self.dp[0] = 0
self.dp[1] = 1
for i in range(2, n+1):
self.dp[i] = self.dp[i-1] + self.dp[i-2]
return self.dp[n]
题目二:凑零钱问题
题目描述:
dp数组解法
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = []
for _ in range(amount + 1):
dp.append(amount + 1)
# 最差结果是只用 1 元硬币凑整,硬币数量为 amount
# dp 数组中全写 amount + 1 的原因是:amount + 1 可以选出 amount
dp[0] = 0
for i in range(len(dp)):
for coin in coins:
if i - coin < 0:
continue
dp[i] = min(dp[i], dp[i-coin] + 1)
# dp[i-coin] + 1:这里的 +1 是因为要加上 coin
# 比较当前策略 dp[i] 和 选择 coin 策略
if dp[amount] == amount + 1:
return -1
# 无变化,说明凑不齐
else:
return dp[amount]
总结
计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。
列出状态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。
备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门。