代码随想录算法day28 | 动态规划算法part01 | 理论基础、509. 斐波那契数、70. 爬楼梯、 746. 使用最小花费爬楼梯

news2024/9/23 5:33:50

理论基础

什么是动态规划

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

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

在贪心算法理论基础中我举了一个背包问题的例子。

例如:有 N 件物品和一个最多能背重量为 W 的背包。第 i 件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中 dp[j] 是由 dp[j-weight[i]] 推导出来的,然后取 max(dp[j], dp[j - weight[i]] + value[i])。

但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

所以贪心解决不了动态规划的问题。

其实大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了

而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。

大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。

动态规划的解题步骤

做动规题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚 dp[i] 表示的是什么。

这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中

状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定 dp 数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?

因为一些情况是递推公式决定了dp数组要如何初始化!

后面的讲解中我都是围绕着这五点来进行讲解。

可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。

其实 确定递推公式 仅仅是解题里的一步而已!

一些同学知道递推公式,但搞不清楚 dp 数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。

动态规划应该如何debug

相信动规的题目,很大部分同学都是这样做的。

看一下题解,感觉看懂了,然后照葫芦画瓢,如果能正好画对了,万事大吉,一旦要是没通过,就怎么改都通过不了,对 dp数组的初始化,递推公式,遍历顺序,处于一种黑盒的理解状态。

写动规题目,代码出问题很正常!

找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!

一些同学对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。

这是一个很不好的习惯!

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果

然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。

如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。

如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。

这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了

这也是我为什么在动规五步曲里强调推导dp数组的重要性。

如果以上自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历 dp 数组的顺序。


509. 斐波那契数

力扣题目链接(opens new window)

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 0 <= n <= 30

斐波那契数列大家应该非常熟悉不过了,非常适合作为动规第一道题目来练练手。

因为这道题目比较简单,可能一些同学并不需要做什么分析,直接顺手一写就过了。

刚好可以通过这道题目让大家可以初步认识到,按照动规五部曲是如何解题的。

对于动规,如果没有方法论的话,可能简单题目可以顺手一写就过,难一点就不知道如何下手了。

动态规划

动规五部曲:

这里我们要用一个一维 dp 数组来保存递归的结果

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

dp[i] 的定义为:第 i 个数的斐波那契数值是 dp[i]

  • 确定递推公式

为什么这是一道非常简单的入门题目呢?

因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];

  • dp数组如何初始化

题目中把如何初始化也直接给我们了,如下:

dp[0] = 0;
dp[1] = 1;
  • 确定遍历顺序

从递归公式 dp[i] = dp[i - 1] + dp[i - 2] 中可以看出,dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

  • 举例推导dp数组

按照这个递推公式 dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当 N 为 10 的时候,dp数组应该是如下的数列:

0 1 1 2 3 5 8 13 21 34 55

如果代码写出来,发现结果不对,就把 dp 数组打印出来看看和我们推导的数列是不是一致的。

以上我们用动规的方法分析完了,Java 代码如下:

class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        int a = 0, b = 1, c = 0;
        for (int i = 1; i < n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列。

递归解法

本题还可以使用递归解法来做

代码如下:

class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        return fib(n - 1) + fib(n - 2);
    }
};

70. 爬楼梯

力扣题目链接(opens new window)

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

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

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

示例 1:

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

示例 2:

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

本题大家如果没有接触过的话,会感觉比较难,多举几个例子,就可以发现其规律。

爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。

那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。

所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。

我们来分析一下,动规五部曲:

定义一个一维数组来记录不同楼层的状态

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

dp[i]: 爬到第 i 层楼梯,有 dp[i] 种方法

  • 确定递推公式

如何可以推出 dp[i] 呢 ?

从 dp[i] 的定义可以看出,dp[i] 可以有两个方向推出来。

首先是 dp[i - 1],上 i-1 层楼梯,有 dp[i - 1] 种方法,那么再一步跳一个台阶不就是 dp[i] 了么。

还有就是dp[i - 2],上 i-2 层楼梯,有 dp[i - 2] 种方法,那么再一步跳两个台阶不就是 dp[i] 了么。

那么 dp[i] 就是 dp[i - 1] 与 dp[i - 2] 之和!

所以 dp[i] = dp[i - 1] + dp[i - 2] 。

在推导 dp[i] 的时候,一定要时刻想着 dp[i] 的定义,否则容易跑偏。

这体现出确定dp数组以及下标的含义的重要性!

  • dp数组如何初始化

再回顾一下 dp[i] 的定义:爬到第 i 层楼梯,有 dp[i] 种方法。

那么 i 为 0,dp[i] 应该是多少呢,这个可以有很多解释,但基本都是直接奔着答案去解释的。

例如强行安慰自己爬到第 0 层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。

但总有点牵强的成分。

那还这么理解呢:我就认为跑到第 0 层,方法就是 0 啊,一步只能走一个台阶或者两个台阶,然而楼层是 0,直接站楼顶上了,就是不用方法,dp[0] 就应该是 0.

其实这么争论下去没有意义,大部分解释说 dp[0] 应该为 1 的理由其实是因为 dp[0]=1 的话在递推的过程中 i 从 2 开始遍历本题就能过,然后就往结果上靠去解释 dp[0] = 1

从dp数组定义的角度上来说,dp[0] = 0 也能说得通。

需要注意的是:题目中说了 n 是一个正整数,题目根本就没说 n 有为 0 的情况。

所以本题其实就不应该讨论 dp[0] 的初始化!

我相信 dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。

所以我的原则是:不考虑 dp[0] 如何初始化,只初始化 dp[1] = 1,dp[2] = 2,然后从 i = 3 开始递推,这样才符合 dp[i] 的定义。

  • 确定遍历顺序

从递推公式 dp[i] = dp[i - 1] + dp[i - 2] 中可以看出,遍历顺序一定是从前向后遍历

  • 举例推导dp数组

举例当 n 为 5 的时候,dp table(dp数组)应该是这样的

70.爬楼梯

如果代码出问题了,就把 dp table 打印出来,看看究竟是不是和自己推导的一样。

此时大家应该发现了,这不就是斐波那契数列么!

唯一的区别是,没有讨论 dp[0] 应该是什么,因为 dp[0] 在本题没有意义!

以上五部分析完之后,Java 代码如下:

class Solution {
    public int climbStairs(int n) {
        if(n <= 2) return n;
        int a = 1, b = 2, sum = 0;

        for(int i = 3; i <= n; i++){
            sum = a + b;  // f(i - 1) + f(i - 2)
            a = b;        // 记录f(i - 1),即下一轮的f(i - 2)
            b = sum;      // 记录f(i),即下一轮的f(i - 1)
        }
        return b;
    }
}

746. 使用最小花费爬楼梯

力扣题目链接

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

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

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

修改之后的题意就比较明确了,题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。

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

使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组 dp[i] 就可以了。

dp[i]的定义:到达第 i 台阶所花费的最少体力为 dp[i]

对于dp数组的定义,大家一定要清晰!

  • 确定递推公式

可以有两个途径得到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[i] 由 dp[i - 1],dp[i - 2] 推出,既然初始化所有的 dp[i] 是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是 dp[0]dp[1] 推出。

那么 dp[0] 应该是多少呢? 根据dp数组的定义,到达第 0 台阶所花费的最小体力为 dp[0],那么有同学可能想,那 dp[0] 应该是 cost[0],例如 cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] 的话,dp[0] 就是 cost[0] 应该是1。

题目描述中明确说了 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。

所以初始化 dp[0] = 0,dp[1] = 0;

  • 确定遍历顺序

最后一步,递归公式有了,初始化有了,如何遍历呢?

本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。

因为是模拟台阶,而且 dp[i] 由 dp[i-1]dp[i-2] 推出,所以是从前到后遍历 cost 数组就可以了。

但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来。 例如:01背包,都知道两个for循环,一个for遍历物品嵌套一个for遍历背包容量,那么为什么不是一个for遍历背包容量嵌套一个for遍历物品呢? 以及在使用一维 dp 数组的时候遍历背包容量为什么要倒序呢?

这些都与遍历顺序息息相关。

  • 举例推导dp数组

拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:

如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的。

以上分析完毕,整体 Java 代码如下:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int len = cost.length;
        int[] dp = new int[len + 1];

        // 从下标为 0 或下标为 1 的台阶开始,因此支付费用为0
        dp[0] = 0;
        dp[1] = 0;

        // 计算到达每一层台阶的最小费用
        for (int i = 2; i <= len; i++) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }

        return dp[len];
    }
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

还可以优化空间复杂度,因为 dp[i] 就是由前两位推出来的,那么也不用dp数组了,Java代码如下:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        // 以下三个变量分别表示前两个台阶的最少费用、前一个的、当前的。
        int beforeTwoCost = 0, beforeOneCost = 0, currentCost = 0;
        // 前两个台阶不需要费用就能上到,因此从下标2开始;因为最后一个台阶需要跨越,所以需要遍历到cost.length
        for (int i = 2; i <= cost.length; i ++) {
            // 此处遍历的是cost[i - 1],不会越界
            currentCost = Math.min(beforeOneCost + cost[i - 1], beforeTwoCost + cost[i - 2]);
            beforeTwoCost = beforeOneCost;
            beforeOneCost = currentCost;
        }
        return currentCost;
    }
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2102541.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于位结构体及位操作总结

#include <stdio.h> #pragma pack(1) struct stu{char a:4; // a占用char的低4位 char b:4; // b占用char的高4位&#xff08;注意&#xff0c;这里实际上是与a共享同一个char的空间&#xff09; }; #pragma pack(4) int main() {struct stu s{.a2, //a:0010.b3, …

如何对单片机程序进行加密(防止别人破解)

单片机程序的破解无非就是非法途径获得源代码或者可执行文件&#xff08;hex文件&#xff09;。本文主要介绍两个方法防止别人从单片机fash中获取可执行文件&#xff08;hex文件&#xff09;。一方面保证别人不能获取你的hex文件&#xff0c;另一面就算别人非法获取你的hex文件…

Windows下的Redis启动报错Redis service failed to start

报错原因&#xff1a;Redis服务没有找到log文件 解决方案&#xff1a; 1、在Redis目录下打开redis.windows-service.conf文件 2、找到logfile存放目录&#xff0c;一般默认为Logs/redis_log.txt 3、在Redis目录创建Logs文件夹&#xff0c;在Logs文件夹下创建redis_log.txt文件…

工业图像输出卡设计原理图:FMC214-基于FMC兼容1.8V IO的Full Camera Link 输出子卡

FMC214-基于FMC兼容1.8V IO的Full Camera Link 输出子卡 一、板卡概述   基于FMC兼容1.8V IO的Full Camera Link 输出子卡支持Base、Middle、Full Camera link信号输出&#xff0c;兼容1.8V、2.5V、3.3V IO FPGA信号输出。适配xilinx不同型号开发板和公司内部各FMC载板。板…

Flutter修改Android包名

一、前言 我在将Android打包上传到google商店的时候提示我“com.example”已受到限制&#xff0c;请换一个软件包名称。“的错误。因此我们需要去修改flutter的Android包名。 二、操作流程 1.修改路径 android ——> app ——> src ——> debug ——> AndroidMa…

LearnOpenGL学习笔记

LearnOpenGL学习笔记 入门认识OpenGL核心模式和立即渲染模式扩展状态机对象 创建窗口视口渲染循环释放资源输入事件渲染 你好&#xff0c;三角形基本概念顶点输入顶点着色器编译着色器片段着色器链接顶点属性顶点数组对象索引缓冲对象 着色器GLSL数据类型输入与输出Uniform 纹理…

深度学习从入门到精通——yolov3算法介绍

YOLO v3 论文地址&#xff1a;https://pjreddie.com/media/files/papers/YOLOv3.pdf论文&#xff1a;YOLOv3: An Incremental Improvement 先验框 (1013)&#xff0c;(1630)&#xff0c;(3323)&#xff0c;(3061)&#xff0c;(6245)&#xff0c;(59 119)&#xff0c; (116 9…

C++代码规范 头文件

1. 头文件 通常每个 .cc 文件应该有一个配套的 .h 文件. 常见的例外情况包括单元测试和仅有 main() 函数的 .cc 文件. 正确使用头文件会大大改善代码的可读性和执行文件的大小、性能. 下面的规则将带你规避头文件的各种误区. 1.1. 自给自足的头文件 Tip 头文件应该自给自…

[某度信息流]SQL164,2021年11月每天新用户的次日留存率

牛客网在线编程 思路&#xff1a; 首先找出用户的注册日期&#xff0c;即date(min(in_time)) 转成date形式 建立两个辅助表&#xff0c;我先放代码&#xff0c;然后进行解释 withuser_reg as (selectuid,date(min(in_time)) as first_datefromtb_user_loggroup by1),…

【路径规划】在二维环境中快速探索随机树和路径规划的示例

摘要 本文介绍了快速探索随机树&#xff08;Rapidly-exploring Random Tree, RRT&#xff09;算法在二维环境中的路径规划应用。RRT是一种随机采样算法&#xff0c;能够快速构建从起点到目标点的路径&#xff0c;特别适用于复杂环境中的机器人路径规划。通过在随机方向上扩展树…

Vue3实时更新时间(年-月-日 时:分:秒)

代码案例 <script lang"ts" setup> import { ref,onMounted } from vue; const timer ref() const date ref("")//年月日 const moreTime ref("")//时分秒 onMounted(()>{//创建定时器1秒执行一次timer.value setInterval(() >…

数学建模强化宝典(10)多元线性回归模型

一、介绍 多元线性回归模型&#xff08;Multiple Linear Regression Model&#xff09;是一种用于分析多个自变量&#xff08;解释变量、预测变量&#xff09;与单个因变量&#xff08;响应变量、被预测变量&#xff09;之间线性关系的统计模型。这种模型假设因变量的变化可以通…

Proxyless的多活流量和微服务治理

1. 引言 1.1 项目的背景及意义 在当今的微服务架构中&#xff0c;应用程序通常被拆分成多个独立的服务&#xff0c;这些服务通过网络进行通信。这种架构的优势在于可以提高系统的可扩展性和灵活性&#xff0c;但也带来了新的挑战&#xff0c;比如&#xff1a; 服务间通信的复…

Excel 将行和列转置的两种方法

方法一&#xff1a; 方法二&#xff1a;使用transpose公式

构造+模拟,CF 873D - Merge Sort

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 873D - Merge Sort 二、解题报告 1、思路分析 考虑初始会调用一次&#…

Opencv中的直方图(3)直方图比较函数compareHist()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 比较两个直方图。 函数 cv::compareHist 使用指定的方法比较两个密集或两个稀疏直方图。 该函数返回 d ( H 1 , H 2 ) d(H_1, H_2) d(H1​,H2​…

虚幻引擎VR游戏开发01 | VR设备和术语

四款Unreal Engine默认配套按键映射的VR设备 IMC按键映射 Oculus Touch (R) Grip Axis: 代表Oculus Rift或Quest设备的右手控制器的抓握轴输入。Valve Index (R) Grip Axis: 代表Valve Index设备的右手控制器的抓握轴输入。Vive (R) Grip: 代表HTC Vive设备的右手控制器的抓握…

[物理专题]经典浮力题目的Fh图像绘制

这段代码用于绘制物体在液体中受到的浮力变化的图像&#xff0c;它有多个好处&#xff1a; 直观展示数据&#xff1a;通过图形化展示&#xff0c;可以直观地看到物体在液体中浸入深度与受到的浮力之间的关系。 教育和学习工具&#xff1a;这种类型的图像常用于教育目的&#x…

不小心删除了 Android 手机上的短信?3 步流程恢复误删除的短信以及图片、视频、联系人

不小心删除了 Android 手机上的短信&#xff1f;别担心&#xff0c;Android 版奇客数据恢复工具可以帮助您通过简单的 3 步流程恢复已删除的短信以及图片、视频、联系人等。 如何在 Android 上恢复已删除的短信 不小心删除了 Android 手机上的短信&#xff1f;Android 版奇客数…

u盘格式化数据还能恢复吗?点击了解实用教程

U盘是电子数据存储设备&#xff0c;我们主要用它来转移数据、随身携带数据等。同时U盘在使用过程中常会遇到问题&#xff0c;比如U盘中毒&#xff0c;U盘中毒会导致里面保存的数据文件无法读取&#xff0c;我们需要进行U盘格式化。格式化之后的U盘才可以继续使用&#xff0c;那…