leetcode刷题记录:动态规划01,斐波那契数列和凑零钱问题

参考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)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值