参考labuladong大神的博客,自己总结的笔记. 原文:https://labuladong.online/algo/di-er-zhan-a01c6/dong-tai-g-a223e/dong-tai-g-1e688/
简介
动态规划题目的一般形式:求最值。
dp三要素:重叠子问题,最优子结构,状态转移方程
例子1 斐波那契数列
https://leetcode.cn/problems/fibonacci-number/
最直接的解法:递归
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
if n == 0:
return 0
elif n == 1:
return 1
else:
return self.fib(n-1) + self.fib(n-2)
时间复杂度: O(2^n)
时间复杂度怎么算的:
画出递归树:
时间复杂度 = 子问题个数 * 解决一个子问题所需要的时间。子问题个数是递归树的所有节点数,二叉树的节点数为指数级别,解决一个子问题的时间只有一个加法操作时1,所以时间复杂度是O(2^n)
优化空间:从递归树里可以看到有很多重复计算,比如第二层F(17)被计算了2次。这就是重叠子问题,下面介绍通过备忘录解法来解决:
带备忘录的递归解法
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
memo = [0 for i in range(n+1)]
return self.dp(memo, n)
def dp(self, memo, n):
if n == 0 or n == 1:
return n
if memo[n] != 0:
return memo[n]
memo[n] = self.dp(memo, n-1) + self.dp(memo, n-2)
return memo[n]
把已经求过的值放在memo中。时间复杂度:一共运行了n次dp,每次的操作时间是常数,时间复杂度为O(n)
空间复杂度:n
这是一种自顶向下的dp;一般更常见的dp是自底向上
用迭代解法,不用递归
可以把dp当成一个table单独拿出来,用自底向上的dp,更好理解
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
# corner case
if n == 0:
return 0
# 长度初始化为n,dp数组从1开始,更好理解
dp = [0 for i in range(n+1)]
# base case
dp[1] = 1
for i in range(2, n+1):
# 状态转移
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
继续优化:空间复杂度度O(1)
因为斐波那契数列的状态转移方程,只和前两个状态有关。dp理论上只用记录2个只即可. 这一般是动态规划的最后一个步骤。
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
if (n <= 1):
return n
dp_i_1, dp_i_2 = 1, 0
dp_i = 0
for i in range(2, n+1):
dp_i = dp_i_1 + dp_i_2
dp_i_2 = dp_i_1
dp_i_1 = dp_i
return dp_i
例子2 凑零钱
https://leetcode.cn/problems/coin-change/
给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
函数签名:
def coinChange(coins: List[int], amount: int) -> int:
这个问题符合最优子结构,因为它的子问题是互相独立的。为什么?因为每种硬币的数量是无限的.
下面是要如何列出状态转移方程
1.base case
amout = 0 时 return 0
2.确定状态,即问题中会变化的量,这里是amount
3.确定选择, 即会让状态变化的量,这里是选择的硬币面值
4.明确dp数组的含义
dp(n),表示要凑到n元钱,最少需要多少个硬币。
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
if (amount == 0):
return 0
dp = [amount+1] * (amount+1) # 重点:初始值设置为amount+1
dp[0] = 0
for i in range(1, amount+1):
for k in coins:
if i < k:
continue
dp[i] = min(dp[i], 1+dp[i-k])
if dp[amount] == amount+1:
return -1
else:
return dp[amount]
问题:为啥 dp 数组中的值都初始化为 amount + 1 呢,因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷。
时间复杂度:O(nk)