代码随想录算法训练营第23期day38|动态规划理论基础、509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯

 目录

一、动态规划理论基础

1.动态规划的解题步骤

2.动态规划应该如何debug

二、(leetcode 509)斐波那契数

1.递归解法

2.动态规划

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

2)确定递推公式

3)dp数组如何初始化

4)确定遍历顺序

5)举例推导dp数组

三、(leetcode 70) 爬楼梯

四、(leetcode 746)使用最小花费爬楼梯

1.新题目描述

2.原题目描述


一、动态规划理论基础

  • 动态规划,英文:Dynamic Programming,简称DP
  • 动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的

1.动态规划的解题步骤

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

为什么要先确定递推公式,然后在考虑初始化呢?因为一些情况是递推公式决定了dp数组要如何初始化!

2.动态规划应该如何debug

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

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

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

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

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

可以自己先思考这三个问题:

  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

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

注意这里不是说不让大家问问题哈, 而是说问问题之前要有自己的思考,问题要问到点子上!

二、(leetcode 509)斐波那契数

力扣题目链接

1.递归解法

class Solution {
public:int fib(int n) {if(n<2) return n;else return fib(n-1)+fib(n-2);}
};
  • 时间复杂度:O(2^n)
  • 空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间

2.动态规划

动规五部曲:这里要用一个一维dp数组来保存递归的结果

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

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

2)确定递推公式

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

3)dp数组如何初始化

题目中把如何初始化也直接给了

dp[0] = 0;
dp[1] = 1;

4)确定遍历顺序

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

5)举例推导dp数组

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

0 1 1 2 3 5 8 13 21 34 55

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

class Solution {
public:int fib(int N) {if (N <= 1) return N;vector<int> dp(N + 1);dp[0] = 0;dp[1] = 1;for (int i = 2; i <= N; i++) {dp[i] = dp[i - 1] + dp[i - 2];}return dp[N];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

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

代码如下:

class Solution {
public:int fib(int N) {if (N <= 1) return N;int dp[2];dp[0] = 0;dp[1] = 1;for (int i = 2; i <= N; i++) {int sum = dp[0] + dp[1];dp[0] = dp[1];dp[1] = sum;}return dp[1];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

三、(leetcode 70) 爬楼梯

力扣题目链接

暗戳戳说,感觉这道题跟上面一道题一样啊,就是有了实际背景!

动规五部曲:

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

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

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

2)确定递推公式

从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] 。

3)dp数组如何初始化

不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。(dp[0]不管怎么定义都很牵强,那就不定义了,就是这么任性!嘻嘻!)

4)确定遍历顺序

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

5)举例推导dp数组

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

70.爬楼梯

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

// 版本一
class Solution {
public:int climbStairs(int n) {if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针vector<int> dp(n + 1);dp[1] = 1;dp[2] = 2;for (int i = 3; i <= n; i++) { // 注意i是从3开始的dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

当然依然也可以,优化一下空间复杂度,代码如下:

// 版本二
class Solution {
public:int climbStairs(int n) {if (n <= 1) return n;int dp[3];dp[1] = 1;dp[2] = 2;for (int i = 3; i <= n; i++) {int sum = dp[1] + dp[2];dp[1] = dp[2];dp[2] = sum;}return dp[2];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

后面的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,但面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,再去优化。因为版本一才能体现出动规的思想精髓,递推的状态变化。

四、(leetcode 746)使用最小花费爬楼梯

力扣题目链接

1.新题目描述

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

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

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

2)确定递推公式

可以有两个途径得到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]);

3)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;

4)确定遍历顺序

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

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

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

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

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

5)举例推导dp数组

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

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

class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {vector<int> dp(cost.size() + 1);dp[0] = 0; // 默认第一步都是不花费体力的dp[1] = 0;for (int i = 2; i <= cost.size(); i++) {dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[cost.size()];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

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

// 版本二
class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int dp0 = 0;int dp1 = 0;for (int i = 2; i <= cost.size(); i++) {int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);dp0 = dp1; // 记录一下前两位dp1 = dpi;}return dp1;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2.原题目描述

旧力扣描述,如果按照第一步是花费的,最后一步不花费,那么代码是这么写的

// 版本一
class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {vector<int> dp(cost.size());dp[0] = cost[0]; // 第一步有花费dp[1] = cost[1];for (int i = 2; i < cost.size(); i++) {dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];}// 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值return min(dp[cost.size() - 1], dp[cost.size() - 2]);}
};

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20.5 OpenSSL 套接字RSA加密传输

RSA算法同样可以用于加密传输&#xff0c;但此类加密算法虽然非常安全&#xff0c;但通常不会用于大量的数据传输&#xff0c;这是因为RSA算法加解密过程涉及大量的数学运算&#xff0c;尤其是模幂运算&#xff08;即计算大数的幂模运算&#xff09;&#xff0c;这些运算对于计…

HTTP 协议请求头 If-Match、If-None-Match 和 ETag

概述 在 HTTP 协议中&#xff0c;请求头 If-Match、If-None-Match、If-Modified-Since、If-Unmodified-Since、If-Range 主要是为了解决浏览器缓存数据而定义的请求头标准&#xff0c;按照协议规范正确的判断和使用这几个请求头&#xff0c;可以更精准的处理浏览器缓存&#x…

【OpenApi Generator】入门和调试

OpenApi Generator是什么 OpenAPI Generator 是一个完全免费开源 (Apache 许可 v2) 的项目&#xff0c;用来生成 REST1 API 客 户端、服务器存根和基于 OpenAPI (以前称为 Swagger ) 规范的文档。如果您不熟悉 OpenAPI 规范&#xff0c;那么它就是描述 RESTful API 方面最流…

【漏洞复现】Apache_HTTPD_换行解析漏洞(CVE-2017-15715)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描3、漏洞验证 1.5、深度利用GetShell 1.6、修复建议 说明内容漏洞编号CVE-2017-15715漏洞名称Ap…

linux下安装Zabbix教程

笔记&#xff1a; 监控设备 对各种设备的统一管理 Esight 了解开源监控工具 eg Promerthos: Zabbix &#xff1a;集中式系统 大型企业 可视化,高大上、 查看日志 安装zibox软件 安装数据库 进入数据库 进入Zabbox 密码 password 账号Admin 密码zabbix 解决乱码问题 将…

HiveSQL中last_value函数的应用

一、背景 在以下数据中如何实现对每一个列按照更新时间取最新的非null值&#xff1f; 1 a a null 202301 202301 1 b b null null 202302 1 null c null null 202303 1 d null null null 202304如何实现…

TFN 2.5G SDH传输分析仪 FT100-D300S

今天给大家带来一款TFN 2.5G SDH传输分析仪--TFN FT100-D300S. D300S SDH测试模块&#xff0c;是FT100智能网络测试平台产品家族的一部分&#xff0c;是一个坚固耐用、锂电池超长供电的传统PDH/SDH测试解决方案&#xff0c;支持2.5Gbps到2.048Mbps速率的传输链路测试。支持在线…

【计算机网络】应用层

应用层协议原理 客户-服务器体系结构&#xff1a; 特点&#xff1a;客户之间不能直接通信&#xff1b;服务器具有周知的&#xff0c;固定的地址&#xff0c;该地址称为IP地址。 配备大量主机的数据中心常被用于创建强大的虚拟服务器&#xff1b;P2P体系结构&#xff1a; 特点&…

【Kubernetes】初识k8s--扫盲阶段

文章目录 1、k8s概述2、为什么要有k8s2.1 回顾以往的应用部署方式2.2 容器具有的优势 3、k8s能带来什么 1、k8s概述 kubernetes是一个可移植、可扩展的开源平台&#xff0c;用于管理 容器化 的工作负载和服务&#xff0c;可促进申明式配置和自动化。kubernetes拥有一个庞大且快…

英伟达发布 Windows 版 TensorRT-LLM 库

导读英伟达发布了 Windows 版本的 TensorRT-LLM 库&#xff0c;称其将大模型在 RTX 上的运行速度提升 4 倍。 GeForce RTX 和 NVIDIA RTX GPU 配备了名为 Tensor Core 的专用 AI 处理器&#xff0c;正在为超过 1 亿台 Windows PC 和工作站带来原生生成式 AI 的强大功能。 Tens…

【日常记录】关于LinkedHashMap中key为数字字符串根据compareTo排序的问题

今天在修复Bug的时候&#xff0c;在项目中发现有这样一段代码&#xff08;为了方便&#xff0c;下面用一个例子进行演示&#xff09;。 项目中使用了LinkedHashMap key为Long类型&#xff0c;value为String类型。 并且&#xff0c;LinkedHashMap 使用Stream流 对key进行升序的排…

项目实战:在首页上添加分页按钮

1、在index.html添加div_pagination <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><link rel"stylesheet" href"style/index.css"><script src&qu…