当前位置: 首页 > article >正文

算法训练营第二十八天 | 动态规划(一)

文章目录

  • 一、动态规划理论基础
    • 动态规划五部曲
      • 确定 dp 数组以及下标的含义
      • 确定递推公式
      • dp 数组初始化
      • 确定遍历顺序
      • 举例推导 dp 数组
  • 二、Leetcode 509. 斐波那契数
  • 三、Leetcode 70.爬楼梯
  • 四、Leetcode 746. 使用最小花费爬楼梯


一、动态规划理论基础

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

动态规划五部曲

确定 dp 数组以及下标的含义

这一步是基础,需要定义一个合适的数据结构(通常是数组或二维数组等)来存储子问题的解,即 dp 数组。dp 数组的每个元素代表一个子问题的解,而其下标的含义则决定了如何描述和区分不同的子问题。例如,在一些问题中,dp[i] 可能表示处理到第 i 个元素时的最优解;在二维情况下,dp[i][j] 可能表示在某种条件下,涉及前 i 个元素且满足另一个维度条件为 j 时的最优解。合理定义 dp 数组及其下标的含义,能清晰地表示问题的状态和子问题之间的关系。

确定递推公式

递推公式是动态规划的核心,它描述了如何从已知的子问题的解推导出当前子问题的解。具体来说,就是根据 dp 数组下标的含义,分析当前状态(子问题)与之前状态(子问题)之间的联系。通常会考虑问题的不同情况或决策,通过对这些情况的分析和组合,得到一个数学表达式来计算 dp 数组中当前位置的值。比如,dp[i] 可能依赖于 dp[i-1]dp[i-2] 等前面位置的值,通过某种运算(如加法、乘法、取最大值等)得到 dp[i] 的值。递推公式的正确性直接决定了动态规划算法能否得到正确的结果。

dp 数组初始化

初始化 dp 数组是为了给动态规划的递推过程提供一个起始点。需要根据问题的实际情况和 dp 数组下标的含义,确定一些最基本的子问题的解。这些基本子问题通常是最简单的情况,比如当 i=0j=0 时的情况。例如,在某些问题中,dp[0] 可能表示初始状态下的解,直接根据问题条件赋值为一个确定的值;在二维 dp 数组中,dp[0][0]dp[0][j](固定 i=0 )或 dp[i][0] (固定 j=0 )等可能需要根据具体情况进行初始化。正确的初始化能确保递推过程从合理的起点开始,避免后续计算出现错误。

确定遍历顺序

遍历顺序决定了如何按照一定的逻辑依次计算 dp 数组中的每个元素。这一步需要考虑 dp 数组中元素之间的依赖关系,即当前元素的计算是否依赖于其他元素已经计算好的值。通常有正向遍历(从小到大)、反向遍历(从大到小)等方式。例如,如果dp[i] 的计算依赖于 dp[i-1] ,那么就需要从 i=1 开始依次递增计算 dp[i] ;在二维数组中,可能需要先遍历一个维度,再遍历另一个维度,或者根据问题的特点采用特殊的遍历顺序。合理的遍历顺序能保证在计算每个 dp 元素时,其所需的依赖值都已经计算完成。

举例推导 dp 数组

这一步是对前面四个步骤的验证和加深理解。通过选取一个简单的具体例子,手动按照前面确定的 dp 数组定义、递推公式、初始化方法和遍历顺序来逐步计算 dp 数组中的值。在推导过程中,可以清晰地看到每个子问题的解是如何得到的,以及整个动态规划的计算过程是否符合预期。如果在推导过程中发现问题,比如递推公式计算结果不符合逻辑、初始化值导致后续计算错误等,可以及时调整前面的步骤,确保动态规划算法的正确性。

二、Leetcode 509. 斐波那契数

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

给定 n ,请计算 F(n)

示例:

原文链接:https://programmercarl.com/0509.%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0.html
题目链接:https://leetcode.cn/problems/fibonacci-number/description/
视频讲解:https://www.bilibili.com/video/BV1f5411K7mo/

动态规划的入门题目,很简单,但是我们还要按照递归五部曲来。

确定 dp 数组以及下标的含义
很显然,本题的递归数组就是这个斐波那契数列,下标对应着第几个元素。

确定递推公式
根据斐波那契数列的定义,我们很容易知道本题的状态方程为:dp[i]=dp[i-1]+dp[i-2]

dp 数组初始化:
根据题目描述,斐波那契数列的第一个元素为 0,第二个元素为 1,故数组的初始化为 dp[0]=0dp[1]=1

遍历顺序
我们需要从前两个元素推出下一个元素,所以我们的遍历顺序是从前到后。

由于遍历中出现了下标 i-1i-2,并且我们数组前两个元素已经被初始化,所以我们遍历的范围是 (2, n)

代码:

class Solution:
    def fib(self, n: int) -> int:
        if n == 0:
            return 0
        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]       

三、Leetcode 70.爬楼梯

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

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

示例:

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

引用:

原文链接:https://programmercarl.com/0070.%E7%88%AC%E6%A5%BC%E6%A2%AF.html
题目链接:https://leetcode.cn/problems/climbing-stairs/description/
视频讲解:https://www.bilibili.com/video/BV17h411h7UH/

动态规划五部曲:

确定 dp 数组以及下标的含义
本题的 dp 数组含义为,爬到第 i 阶楼梯需要多少步。

确定递推公式
本题目的难点在于确定递推公式。

我们爬到 i-1 阶的时候,需要 dp[i-1] 多步,当我们下一步想迈一阶的时候,那么我们趴到 i 阶就需要 dp[i-1] 步。
我们爬到 1-2 阶的时候,需要 dp[i-2] 步,当我们下一步想要爬两阶的时候,那么我们趴到 i 阶就需要 dp[i-2] 步。
所以我们第 i 阶共有两种选择,那么第 i 阶就一共需要 dp[i-1] + dp[i-2] 步。
故递推公式为 dp[i]=dp[i-1]+dp[i-2]

dp 数组初始化:
n 等于 0 的时候没有任何意义,所以我们不对 dp[0]初始化,由题目描述很容易得知数组的初始化为 dp[1]=1dp[2]=2

遍历顺序
当前状态需要从前两个状态推出,所以我们的遍历顺序为从前到后。

由于遍历中出现了下标 i-1i-2,并且我们数组前三个元素已经被初始化,所以我们遍历的范围是 (3, n)

代码:

class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        dp = [0] * (n+1)
        dp[1] = 1
        dp[2] = 2
        
        for i in range(3, n+1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[n]

四、Leetcode 746. 使用最小花费爬楼梯

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

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

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

示例:

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

引用:

原文链接:https://programmercarl.com/0746.%E4%BD%BF%E7%94%A8%E6%9C%80%E5%B0%8F%E8%8A%B1%E8%B4%B9%E7%88%AC%E6%A5%BC%E6%A2%AF.html
题目链接:https://leetcode.cn/problems/min-cost-climbing-stairs/description/
视频讲解:https://www.bilibili.com/video/BV16G411c7yZ/

动态规划五部曲:

确定 dp 数组以及下标的含义
本题的 dp 数组含义为,爬到第 i 阶楼梯需要花费多少钱

确定递推公式

可以有两个途径得到 dp[i],一个是 dp[i-1] 一个是 dp[i-2]

dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]

dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]

那么究竟是选从dp[i - 1]跳还是从 dp[i - 2] 跳呢?

一定是选最小的,所以 dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) ;

dp 数组初始化:
只初始化 dp[0]dp[1] 就够了,其他的最终都是 dp[0] dp[1]推出。我们默认在第一个台阶是不需要花费的

遍历顺序
当前状态需要从前两个状态推出,所以我们的遍历顺序为从前到后。

由于遍历中出现了下标 i-1i-2,并且我们数组前两个元素已经被初始化,所以我们遍历的范围是 (2, len(cost)+1)

代码:


class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        dp = [0] * (len(cost) + 1)
        dp[0] = 0  # 初始值,表示从起点开始不需要花费体力
        dp[1] = 0  # 初始值,表示经过第一步不需要花费体力
        
        for i in range(2, len(cost) + 1):
            # 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步
            # 选择其中花费体力较小的路径,加上当前步的花费,更新dp数组
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
        
        return dp[len(cost)]  # 返回到达楼顶的最小花费

http://www.kler.cn/a/614785.html

相关文章:

  • WinForm真入门-简介
  • NLP语言模型训练里的特殊向量
  • Linux系统中应用端控制串口的基本方法
  • 数据结构----栈
  • 记录vite引入sass预编译报错error during build: [vite:css] [sass] Undefined variable.问题
  • resnet网络迁移到昇腾执行(OM上篇)
  • 基于三维数字图像相关(DIC)全场应变测量技术的基础设施结构健康监测与安全评估方法研究
  • 探索Scala基础:融合函数式与面向对象编程的强大语言
  • 【人工智能】解锁大模型潜力:Ollama 与 DeepSeek 的分布式推理与集群部署实践
  • 智慧养老线上线下联动:重构多样化养老服务的创新实践
  • 【Qt】数据库管理
  • 从零开始研发GPS接收机连载——19、自制GPS接收机的春运之旅
  • WebSocket通信的握手阶段
  • 图像数据增强教程:为目标检测任务准备数据
  • 【可视化教程】密码验证(栈)【算法竞赛】
  • 业务流程先导及流程图回顾
  • hugo+github pages 部署实验室网站
  • 用 pytorch 从零开始创建大语言模型(三):编码注意力机制
  • Ubuntu 22.04 安装向日葵远程控制
  • linux系统中fstab 各字段详细说明