算法学习 day32 动态规划01

59 阅读7分钟

理论基础

动态规划通常用来解决含重叠子问题的问题,动态规划中的状态可以通过前一个时刻的状态推导。解题步骤有5步:

1 确定dp数组定义和下标i的含义。 2 递推公式。 3 dp初始化。 4 递推遍历的方向。 5 举例推导

debug时打印dp数组看看与推导的是否一致,如果一致还是错了说明dp的递推公式或者初始化等条件出错了。

509. 斐波那契数

[文章讲解](programmercarl.com/0509.%E6%96…

视频讲解

题目:斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

 

示例 1:

输入: n = 2
输出: 1
解释: F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入: n = 3
输出: 2
解释: F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入: n = 4
输出: 3
解释: F(4) = F(3) + F(2) = 2 + 1 = 3

 

提示:

  • 0 <= n <= 30

解题思路1: 自顶向下递归

使用memo记录已经算过的fib(n)从而剪枝,跳过冗余的计算。

  • 时间复杂度:O(n), 没有memo的fib是O(2^n)复杂度,带有memo的fib只要计算n次。

  • 空间复杂度:O(n),递归栈最大深度是n,memo也是n,所以是O(n)。

class Solution:
    memo =  {0:0, 1:1} # dp[n]对应的值
    
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        
        if n not in self.memo:
            res = self.fib(n-1) + self.fib(n-2)
            self.memo[n] = res
        return self.memo[n]

解题思路2: 自底向上递推(动态规划)

递推:

1 dp数组定义和下标i的含义: dp[i]表示第i个斐波那契数 size=n+1

2 递推公式:dp[i]=dp[i-1]+dp[i-2]

3 dp初始化: dp[0] = 0 dp[1]=1

4 递推遍历的方向: 从前到后

5 举例推导: n=5 0 1 1 2 3 5

  • 时间复杂度:O(n),递推n次。

  • 空间复杂度:O(n), dp数组大小。

class Solution:
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        
        # 初始化
        dp = [0] * (n+1)
        dp[0] = 0
        dp[1] = 1

        # 递推
        for i in range(2, n+1):
            dp[i] = dp[i-1] + dp[i-2]
        
        return dp[n]

解题思路3: 动态规划 状态压缩

由于dp[i]仅仅依赖于dp[i-1]和dp[i-2],可以使用pre和cur替代两个状态,删除原先O(n)的dp数组空间开销。

  • 时间复杂度:O(n),

  • 空间复杂度:O(1)。

class Solution:
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        
        # 初始化
        pre = 0
        cur = 1
        # 递推
        for i in range(2, n+1):
            val = pre + cur
            pre = cur
            cur = val

        return cur

总结

70. 爬楼梯

文章讲解

视频讲解

题目:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

 

示例 1:

输入: n = 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入: n = 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

 

提示:

  • 1 <= n <= 45

解题思路1: 自顶向下递归

本题和斐波那契类似,达到climbStairs(n)有两种方法,第一种是从n-1走1步,第二种是从n-2走2步,得到递推关系:dp[i] = dp[i-2] + dp[i-1], 注意dp[0]=1。

使用memo记录已经算过的climbStairs(n)从而剪枝,跳过冗余的计算。

  • 时间复杂度:O(n), 没有memo的climbStairs是O(2^n)复杂度,带有memo的climbStairs只要计算n次。

  • 空间复杂度:O(n),递归栈最大深度是n,memo也是n,所以是O(n)。

class Solution:
    memo = {0:1, 1:1}

    def climbStairs(self, n: int) -> int:
        if n < 2:
            return self.memo[n]
        if n not in self.memo:
            res = self.climbStairs(n-1) + self.climbStairs(n-2)
            self.memo[n] = res
        
        return self.memo[n]

解题思路2: 自底向上递推(动态规划)

递推:
1 dp数组定义和下标i的含义: dp[i]表示爬到第i层楼需要的步数 size=n+1
2 递推公式:dp[i]= dp[i-2]  +  dp[i-1] # 到dp[i]时有两种走法:走1步或者走两步,对应dp[i-1]次数走1步,和dp[i-2]走两步
3 dp初始化: 
    dp[0] = 1 
    dp[1] = 1 
    dp[2]= 2 # 走2x1 1x2
4 递推遍历的方向: 从前到后
5 举例推导: n=5  
    dp[0] = 1 
    dp[1] = 1 
    dp[2]= 2
    dp[3] = 3 
    dp[4] = 5
    dp[5] = 8
  • 时间复杂度:O(n),递推n次。

  • 空间复杂度:O(n), dp数组大小。

class Solution:
    def climbStairs(self, n: int) -> int:
        # 初始化
        dp = [0] * (n+1)
        dp[0] = dp[1] = 1
        if n < 2:
            return dp[n]
        
        # 递推
        for i in range(2, n+1):
            dp[i] = dp[i-1] + dp[i-2] # 走1步或2步
        
        return dp[n]

解题思路3: 动态规划 状态压缩

由于dp[i]仅仅依赖于dp[i-1]和dp[i-2],可以使用pre和cur替代两个状态,删除原先O(n)的dp数组空间开销。

  • 时间复杂度:O(n),

  • 空间复杂度:O(1)。

class Solution:
    def climbStairs(self, n: int) -> int:
        # 初始化
        pre = 1
        cur = 1

        # 递推
        for i in range(2, n+1):
            val = pre + cur
            pre = cur
            cur = val
        
        return cur

总结

57. 爬楼梯(第八期模拟笔试)

文章讲解

视频讲解

题目:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢? 

注意:给定 n 是一个正整数。

输入描述:

输入共一行,包含两个正整数,分别表示n, m

输出描述:

输出一个整数,表示爬到楼顶的方法数。

输入示例:

3 2

输出示例:

3

提示信息:

数据范围:
1 <= m < n <= 32;

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶段
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

解题思路

dp[i] = dp[i-1] + dp[i-2] + ... + dp[i-m]

  • 时间复杂度:O(n),

  • 空间复杂度:O(n),。

import sys

def climb_stairs(n, m):
    # 创建一个数组来存储到达每一阶的方法数
    dp = [0] * (n + 1)
    dp[0]=1  # 到达第0阶的方法数为1

    # 计算每一阶的方法数
    for i in range(1, n+1):
        for j in range(1, min(m,i)+1):
            dp[i] += dp[i-j]

    return dp[n]

# 输入
n, m = map(int, sys.stdin.readline().split())
# 输出结果
print(climb_stairs(n, m))

总结

746. 使用最小花费爬楼梯

文章讲解

视频讲解

题目:给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

 

示例 1:

输入: cost = [10,15,20]
输出: 15
解释: 你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入: cost = [1,100,1,1,1,100,1,1,100,1]
输出: 6
解释: 你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

 

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999

解题思路

dp[i]表示达到第i个台阶花费的体力值cost

注意:

1 第i个台阶的cost[i]暂时不计入,只有跨过i时候才要dp[i] + cost[i]。

2 第0和1个台阶不算cost。

递推:

dp[i] = min(dp[i-1] + cost[i-1], dp[i-2]+cost[i-2])

  • 时间复杂度:O(n)。

  • 空间复杂度:O(n)。

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        n = len(cost)
        # 初始化
        dp = [0] * (n+1)
        dp[0], dp[1] = 0, 0

        for i in range(2, n+1):
            dp[i] =  min(dp[i-1] + cost[i-1], dp[i-2]+cost[i-2])
        
        return dp[n]

总结