力扣力扣力:动态规划入门(1)

        相信大家在第一次学动态规划的时候都是一脸懵逼的,在看了很多题解之后,陷入到了空的“最优子结构”等的大词上,依旧看不懂动态规划到底在干什么。今天我们也是老样子再一次的从零开始学习与讲解,俺也是从零开始学动态规划,所以学多少写多少,和大家一起领悟动态规划最自然的想法。

我们为什么需要动态规划:

        在搞清楚动态规划怎么实现之前,我们先搞清楚我们为什么需要它。我们都知道在算法的世界里面,一般分为两种问题,一种是有确定算法流程,确定的计算过程,计算机按照代码顺序执行就一定能拿到结果的,例如图论里面的最小生成树、最短路径等算法;还有一种是我们拿不到这种确切的计算过程,所以我们只能按照一定的顺序一个个去试出我们想要的结果,例如经典的递归算法。

        这两种问题对应到数学问题中, 就好像一种是我们知道数列的通项公式,一种是我们知道了数列的递推式。例如最简单的通向公式等差数列:an = a1+(n-1)*d,又例如最经典的递推公式斐波那契数列:F(n) = F(n - 1) + F(n - 2)。动态规划要解决的问题就是后者,而后者大多都能用递归来解决,动态规划要干的事情就是用空间来储存递归过程中重复计算的子问题,来减少计算过程优化流程。所以只要是一个动态规划能解决的问题就一定对应着一个重复调用的递归子问题,动态规划能解决的递归也同样能够解决。实际上我们在进行流程优化的时候经常会利用上述缓存已经出现过的结果这种思想,在计算机网络、操作系统中等很多对时间要求高的地方可以看到这种操作。

ok,在简单了解之后我们做三道简单经典题目,来通过不断优化流程去逐步感受上述的思想。动态规划这类的算法题不在多在于弄懂原理,因为每一道题都不一样都得具体问题具体分析。

斐波那契数列:509. 斐波那契数 - 力扣(LeetCode)

这里我们就用上面给出的递推例子斐波那契额数列来直观感受递推思想的优化流程。

递归版:

我们先给出最常见的递归写法,相信大家这种应该都会。

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

我们以计算fib(4)小一点的数字来举例,下面给出递归的展开二叉树图

            fib(4)/       \fib(3)       fib(2)/      \       /     \fib(2)   fib(1) fib(1) fib(0)/      \
fib(1) fib(0)

这种递归的时间复杂度显然是O(2^N),接下来我们开始进行优化递归过程。

        首先我们可以发现,fib(2),fib(1),fib(0)这几个重复计算了好多次,这也就是我们上面说的重复计算的递归子问题,所以这里存在优化的空间。我们要想能不能直接储存起来之前算过的值,这样如果出现第二次多次计算的时候直接调用前面存起来的值不就好了。下面给出缓存后的二叉树展开图:

            fib(i)/       \fib(i-1)       fib(i-2)/      \            fib(i-2)   fib(i-3)/      \
fib(i-3) fib(i-4)/
fib(i-4)
...
...

        我们可以直观的发现我们其实只需要自底向上计算最左边的一列加上其右子树例如fib(i-3)和fib(i-4),所以这个时候时间复杂度就很直观的降到了O(N)。这就是优化进步的地方,所以其实本质上干的事情就是拿空间的储存结构去换取时间上的复杂度优化。

        所以这里我们需要一个数据结构来储存计算过的值,我们在动态规划中一般称这种表为dp表。而我们接下来要做的事情就是填满这张表返回表的最后一个元素即可。所以在动态规划中最重要的一步就是知道dp表怎么来的表示什么意思,我们一般有以下几种方式:1.题目要求 2.经验+题目要求 3.分析问题的过程中,发现了重复的子问题。我们这道题由于比较简单题目直接给出了,其实按照上述分析也可以算发现重复子问题。

        其次才是最难的得出状态转移方程,也就是如何填上我们的这张表。由于本道题很简单,状态转移方程说人话就是求出递推式。这里题目也直接给出了就是F(n) = F(n - 1) + F(n - 2)。

        然后就是对表进行初始化过程,这个过程主要是用来保证不会越界访问。例如当n=0或者1的时候取到了-1和-2这种值就已经越界访问了。

动态规划版:

class Solution {
public:int fib(int n) {//1.创建dp表//2.初始化表//3.填表//4.返回值//先处理前两项情况if(n == 0) return 0;if(n == 1) return 1;vector<int> dp(n+1);//由于题目下标是从0开始,所以是n+1dp[0] = 0 , dp[1] = 1;for(int i=2;i<=n;i++)dp[i] = dp[i-1] + dp[i-2];return dp[n]; }
};

ok我们现在已经对时间复杂度进行了很大的优化,算到这里还能不能进一步优化。

其实是能的,我们还可以对空间进行优化。我们在填表的时候会发现其实我们计算每一个格子的时候只利用到前两个,之前的都利用不到了。又由于我们只需要拿到最终结果也就是最后一个元素,所以前面格子的数据不需要保存。所以实际上我们只需要定义三个变量就能解决了,从原来的O(N)的空间复杂度进一步优化到O(1)常数级复杂度。这个方法也一般被称为滚动数组,在背包问题中会经常用到。

滚动数组版:

class Solution {
public:int fib(int n) {//1.创建dp表//2.初始化表//3.填表//4.返回值//先处理前两项情况if(n == 0) return 0;if(n == 1) return 1;int a = 0, b = 1, c = 0;for(int i=2;i<=n;i++){//先计算下一个数c = a + b;//滚动操作a = b, b = c;}return c; }
};

当然由于这道题是斐波那契额数列的特殊性,我们可以通过数学的方法得到通项公式,大多数题目是无法做到这一点的,这里我们不再过多讨论这种方法。

三步问题:面试题 08.01. 三步问题 - 力扣(LeetCode)

        关于这道题,首先我想说的一点是一般对于动态规划的题目我们从后往前思考。也就是如果我们想要计算n,是不是先要计算n-1,想要得到n-1是不是先要得到n-2。如果我们一上来按照正常人思考的模式从前往后数的话,前几个台阶还可以理清楚,到后面方法一多很容易数错,从而导致归纳错误。如果能数对的话,其实也可以通过找规律来得到状态转移方程。而从后往前计算从本质上是符合递归计算的顺序的,递归计算是自底向上计算得到的。

        在明确了思考方向后,我们就很容易得到思路,想要到第n阶台阶,前一步有三种可能性,即:前一步已经到达第n-1、n-2、n-3阶台阶三种可能,至于为什么是三种其实是因为小孩一次只能最大跳3阶,如果是4或者5阶我们也可以同步进行变化。下面给出实现:

class Solution {
public:int waysToStep(int n) {if(n==1 || n==2) return n;if(n==3) return 4;const int MOD = 1e9+7;vector<int> dp(n+1);dp[1] = 1,dp[2] = 2,dp[3] = 4;for(int i=4;i<=n;i++)dp[i] = ((dp[i-1] + dp[i-2]) % MOD + dp[i-3]) % MOD;return dp[n];}
};

唯一需要注意的是这道题由于数字可能会很大需要%一下mod

最小花费爬楼梯:LCR 088. 使用最小花费爬楼梯

我们还是一样的从后往前分析,如果要爬完楼梯,由于一次只能爬一到两步,那么前一步就只有两种可能,一种是离楼顶只有一阶台阶,还有一种是两阶。

那么爬到楼顶的最小花费就是这两种方法中的小的那个。

dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]):到达前一步再加上最后一步的花费即可。

版本一:

class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();vector<int> dp(n+1);for(int i=2;i<=n;i++)dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);return dp[n];}
};

与之前不同的是,这道题似乎没有初始化数组,这是因为这道题初始化dp[0]=0,dp[1]=0,而调用vector的构造函数会自动填上0,所以省去这一步。

版本二:

        版本一的dp[i]的含义是到i位置的最小花费,所以顾名思义就是以i为终点的最小花费,这里我们再介绍另一种以i为起点到台阶顶的最小花费。和前面一样还是只分为两种可能:

所以还是一样,最小花费依旧是这二者的min。另外这里的初始化也要从最后两个格子开始初始化,初始化的值就为该位子的cost,其次填表的顺序也要相同的从后往前开始填。最后给出实现:

class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();vector<int> dp(n);dp[n-1] = cost[n-1];dp[n-2] = cost[n-2];for(int i=n-3;i>=0;i--)dp[i] = cost[i]+min(dp[i+1],dp[i+2]);return min(dp[0],dp[1]);}
};

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

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

相关文章

私域流量时代下的新型商业模式:以开源链动 2 + 1 模式、AI 智能名片、S2B2C 商城小程序源码为例

摘要&#xff1a;本文探讨了私域流量时代的特点及其对商业盈利模式的影响。通过分析从大众消费时代到私域流量时代的转型&#xff0c;阐述了商品到“人”的变化过程。同时&#xff0c;深入研究了开源链动 2 1 模式、AI 智能名片和 S2B2C 商城小程序源码在私域流量发展中的作用…

多模态交互智能体全面解析:定义、架构、学习机制、系统实现、分类、应用场景及评估方法

多模态AI系统很可能会成为我们日常生活中无处不在的存在。使这些系统更具交互性的一种有希望的方法是将它们作为物理和虚拟环境中的智能体。目前&#xff0c;系统利用现有的基础模型作为创建具身智能体的基本构建块。将智能体嵌入这些环境中&#xff0c;有助于模型处理和解释视…

助眠白噪音视频素材哪里找?这些平台帮你快速找到放松素材

在现代社会&#xff0c;信息的轰炸让人们的生活节奏变得越来越快&#xff0c;很多人晚上都在床上辗转反侧&#xff0c;难以入眠。如果你也遇到这样的困扰&#xff0c;想要寻找助眠的白噪音视频素材&#xff0c;那么今天介绍的这些网站将会是你的福音&#xff01;它们提供高质量…

一年四起供应链投毒事件的幕后黑手

前言 2017年&#xff0c;黑客入侵Avast服务器&#xff0c;在CCleaner更新中植入恶意代码&#xff0c;被数百万用户下载。 2017年&#xff0c;M.E.Doc遭黑客攻击&#xff0c;篡改更新植入NotPetya&#xff0c;影响全球公司。 2020年&#xff0c;黑客入侵SolarWinds服务器&…

Qt信号和槽-->day04

Qt信号和槽 标准的信号和槽函数Qt中的槽函数Qt中的信号 connect案例 自定义信号和槽案例分析 信号槽的拓展信号连接信号案例 信号槽的两种连接方式Qt5中的处理方式Qt4中的处理方式Qt5处理信号槽重载问题案例 lambda表达式简单案例Qt中的应用 补充知识点 标准的信号和槽函数 QW…

【超级详细】基于Zynq FPGA对雷龙SD NAND的测试

目录 一、SD NAND特征1.1 SD卡简介1.2 SD卡Block图 二、SD卡样片三、Zynq测试平台搭建3.1 测试流程3.2 SOC搭建 一、SD NAND特征 1.1 SD卡简介 雷龙的SD NAND有很多型号&#xff0c;在测试中使用的是CSNP4GCR01-AMW与CSNP32GCR01-AOW。芯片是基于NAND FLASH和 SD控制器实现的…

linux物理内存管理:node,zone,page

一、总览 对于物理内存内存&#xff0c;linux对内存的组织逻辑从上到下依次是&#xff1a;node&#xff0c;zone&#xff0c;page&#xff0c;这些page是根据buddy分配算法组织的&#xff0c;看下面两张图&#xff1a; 上面的概念做下简单的介绍&#xff1a; Node&#xff1a…

STM32-Flash闪存

目录 一、简介 1、闪存模块组织 2、FLASh基本结构 3、FLash写入和读取操作 4、编程流程 5、选项字节格式 6、选项字节编程步骤 二、读写芯片内部FLASH编程 三、器件电子签名 1、简介 2、编程实现 一、简介 STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节…

数据结构之带头双向循环链表

前言&#xff1a;前面我们实现了顺序表和单链表&#xff0c;这次来实现一个结构更复杂的链表-----带头双向循环链表。不要被它的名字吓到哦&#xff0c;只是结构复杂而已&#xff0c;它的结构更有利于代码的实现。 1 双向循环链表的介绍 有了单链表的基础&#xff0c;要实现这…

10个最常用的Python包,程序员必备!

世界上有超过200,000个Python程序包&#xff08;这只是基于官方的Python程序包索引PyPI托管的程序包&#xff09;。 这就引出了一个问题&#xff1a;拥有这么多的软件包&#xff0c;每个Python程序员都需要学习哪些软件包是最重要的&#xff1f; 为了帮助回答这个问题&#x…

线上问题的排查-java死锁问题如何排查

这里写目录标题 1.java死锁如何排查2.具体步骤1.1识别死锁现象1.2收集线程转储1.3分析线程转储1.4代码审查1.5重现问题1.6使用调试工具1.7.优化和验证 3. 解决方案总结 1.java死锁如何排查 在Java应用程序中&#xff0c;死锁是一个经典的并发问题&#xff0c;它会导致线程永久阻…

shodan(4-5)

以下笔记学习来自B站泷羽Sec&#xff1a; B站泷羽Sec 1. 查看自己的出口IP 可以知晓自己是哪个IP连接的公网 shodan myip2. 指定标题搜索 http.title:内容搜索被挂黑页的网页&#xff1a;获得标题中含有hacked by的IP shodan search --limit 10 --fields ip_str,port htt…

三种风格截然不同的实验室工控界面

三种风格截然不同的实验室工控界面各具特色。一种可能是简洁现代风&#xff0c;以简洁的线条、纯净的色彩和直观的图标&#xff0c;呈现出高效与专业。 另一种或许是科技未来风&#xff0c;运用炫酷的光影效果和立体感十足的设计&#xff0c;展现实验室的前沿科技感。 还有一…

Redis如何保证数据不丢失(可靠性)

本文主要以学习为主&#xff0c;详细参考&#xff1a;微信公众平台 Redis 保证数据不丢失的主要手段有两个&#xff1a; 持久化 多机部署 我们分别来看它们两的具体实现细节。 1.Redis 持久化 持久化是指将数据从内存中存储到持久化存储介质中&#xff08;如硬盘&#xf…

STM32F405RGT6单片机原理图、PCB免费分享

大学时机创比赛时画的板子&#xff0c;比到一半因为疫情回家&#xff0c;无后续&#xff0c;&#xff0c;&#xff0c;已打板验证过&#xff0c;使用stm32f405rgt6做主控 下载文件资源如下 原理图文件 pcb文件 外壳模型文件 stm32f405例程 功能 以下功能全部验证通过 4路…

“穿梭于容器之间:C++ STL迭代器的艺术之旅”

引言&#xff1a; 迭代器&#xff08;Iterator&#xff09;是C STL&#xff08;标准模板库&#xff09;中非常重要的一部分&#xff0c;它提供了一种统一的方式来遍历容器中的元素。无论容器是数组、链表、树还是其他数据结构&#xff0c;迭代器都能够以一致的方式访问这些数据…

jmeter常用配置元件介绍总结之用linux服务器压测

系列文章目录 安装jmeter jmeter常用配置元件介绍总结之用linux服务器压测 1.编写测试脚本2.执行测试脚本 1.编写测试脚本 在linux服务器上进行压测&#xff0c;由于是没有界面的&#xff0c;因此我们可以先在界面上把压测脚本写好&#xff1a; 如图&#xff1a;我这里简单的写…

Ubuntu 的 ROS 操作系统安装与测试

引言 机器人操作系统&#xff08;ROS, Robot Operating System&#xff09;是一个用于开发机器人应用的开源框架&#xff0c;它提供了一系列功能丰富的库和工具&#xff0c;能够帮助开发者构建和控制机器人。 当前&#xff0c;ROS1的最新版本为Noetic Ninjemys&#xff0c;专为…

计算机组成原理——编码与纠错(汉明编码)

校验码放在2^x次方的位置——即1&#xff0c;2&#xff0c;4——将检测位按序排列p3p2p1 汉明编码从左到右数某个位置位1&#xff08;位数&#xff09;&#xff0c;就表示第几组 奇偶校验 例题 纠错过程 汉明编码的最小距离是3

fabric操作canvas绘图(1)共32节

对于前端而言&#xff0c;离不开canvas就像鱼离不开水&#xff0c;前端canvas神器fabric你值得拥有&#xff01;接下来我们就来一步步揭开她的面纱。 一、fabric的理解 用原生的canvas来实现&#xff0c;代码量会比较大&#xff0c;而且还要处理很多细节&#xff0c;而Fabric…