《剑指 Offer》专项突破版 - 面试题 88 : 动态规划的基础知识(C++ 实现)

目录

前言

面试题 88 : 爬楼梯的最少成本

一、分析确定状态转移方程

二、递归代码

三、使用缓存的递归代码

四、空间复杂度为 O(n) 的迭代代码

五、空间复杂度为 O(1) 的迭代代码



前言

动态规划是目前算法面试中的热门话题,应聘者经常在各大公司的面试中遇到需要运用动态规划才能解决的问题。由于动态规划相关的面试题题型变化多样,有时让人琢磨不透,因此很多应聘者认为动态规划是算法面试中的一个难点。

其实,在深入理解动态规划之后就能发现其实运用动态规划解决算法面试题是有套路的。运用动态规划解决问题的第 1 步是识别哪些问题适合运用动态规划。和适用运用回溯法的问题类似,适用动态规划的问题都存在若干步骤,并且每个步骤都面临若干选择。如果题目要求列举出所有的解,那么很可能需要用回溯法解决。如果题目是求一个问题的最优解(通常是最大值或最小值),或者求问题的解的数目(或判断问题是否存在解),那么这个题目有可能适合运用动态规划

例如,给定一个没有重复数字的正整数集合,请列举出所有元素之和等于某个给定值的所有组合。同一个数字可以在组合中出现任意次。例如,输入整数集合 [2, 3, 5],元素之和等于 8 的组合有 3 个,分别是 [2, 2, 2, 2]、[2, 3, 3] 和 [3, 5]。

这个题目要求列举出所有符合条件的组合,即找出问题的所有解,可以用回溯法解决这个问题。

又如,给定一个没有重复数字的正整数集合,请找出所有元素之和等于某个给定值的所有组合的数目。同一个数字可以在组合中出现任意次。例如,输入整数集合 [2, 3, 5],组合 [2, 2, 2, 2]、[2, 3, 3] 和 [3, 5] 的和都是 8,因此输出组合的数目 3。

这个题目看起来和前一个很相像,但它们有一个根本区别:第 1 个题目要求列举出所有的组合,因此很适合采用回溯法;第 2 个题目只需要求出符合条件的组合的数目,对具体的每个组合不感兴趣,因此可以采用动态规划解决这个问题。

在采用动态规划时总是用递归的思路分析问题,即把大问题分解成小问题,再把小问题的解合起来形成大问题的解。找出描述大问题的解和小问题的解之间递归关系的状态转移方程是采用动态规划解决问题的关键所在。下面将按照 "单序列问题"、"双序列问题"、"矩阵路径问题" 和 "背包问题" 等常见题型详细讨论如何采用递归的思路分析问题并最终运用动态规划解决问题。

分治法也是采用递归思路把大问题分解成小问题。例如,快速排序算法就是采用分治法。分治法将大问题分解成小问题之后,小问题之间没有重叠的部分。例如,快速排序算法将一个数组分成两个子数组,然后排列两个子数组,这两个子数组之间没有重叠的部分。如果应用递归思路将大问题分解成小问题之后,小问题之间没有相互重叠的部分,那么可以直接写出递归的代码实现相应的算法

如果将大问题分解成小问题之后,小问题相互重叠,那么直接用递归的代码实现就会存在大量重复计算。小问题之间存在重叠的部分,这是可以运用动态规划求解问题的另一个显著特点

在用代码实现动态规划的算法时,如果采用递归的代码按照从上往下的顺序求解,那么每求出一个小问题的解就缓存下来,这样下次再遇到相同的小问题就不用重复计算。另一个实现动态规划算法的方法是按照从下往上的顺序,从解决最小的问题开始,并把已经解决的小问题的解存储下来(大部分面试题都存储在一维数组或二维数组中),然后把小问题的解组合起来逐步解决大问题

下面通过一个具体的例子来讨论应用动态规划分析和解决问题的过程。


面试题 88 : 爬楼梯的最少成本

题目

一个数组 cost 的所有数字都是正数,它的第 i 个数字表示在一个楼梯的第 i 级台阶往上爬的成本,在支付了成本 cost[i] 之后可以从第 i 级台阶往上爬 1 级或 2 级。假设台阶至少有 2 级,既可以从第 0 级台阶出发,也可以从第 1 级台阶出发,请计算爬上该楼梯的最少成本。例如,输入数组 [1, 100, 1, 1, 100, 1],则爬上该楼梯的最少成本是 4,分别经过下标 0、2、3、5 这 4 级台阶,如下图所示。

分析

爬上一个有多级台阶的楼梯自然需要若干步。按照题目的要求,每次爬的时候既可以往上爬 1 级台阶,也可以爬 2 级台阶,也就是每一步都有两个选择。这看起来像是与回溯法相关的问题。但这个问题不是要找出有多少种方法可以爬上楼梯,而是计算爬上楼梯的最少成本,即计算问题的最优解,因此,这个问题更适合运用动态规划

一、分析确定状态转移方程

这个问题要求计算爬上楼梯的最少成本,可以用函数 f(i) 表示从楼梯的第 i 级台阶再往上爬的最少成本(注意:已经支付了成本 cost[i])。如果一个楼梯有 n 级台阶(台阶从 0 开始计数,从第 0 级一直到第 n - 1 级),由于一次可以爬 1 级或 2 级台阶,因此最终可以从第 n - 2 级台阶或第 n - 1 级台阶爬到楼梯的顶部,即 f(n - 1) 和 f(n - 2) 的最小值就是这个问题的最优解

应用动态规划的第 1 步是找出状态转移方程,即用一个等式表示其中某一步的最优解和前面若干步的最优解的关系。根据题目的要求,可以一次爬 1 级或 2 级,既可以从第 i - 1 级台阶爬上第 i 级台阶,也可以从第 i - 2 级台阶爬上第 i 级台阶,因此,从第 i 级台阶往上爬的最少成本应该是从第 i - 1 级台阶往上爬的最少成本和从第 i - 2 级台阶往上爬的最少成本的较小值再加上在第 i 级台阶往上爬的成本。这个关系可以用状态转移方程表示为 f(i) = min(f(i - 1), f(i - 2)) + cost[i]

上述状态转移方程有一个隐含的条件,即 i 大于或等于 2。如果 i 等于 0,则可以直接从第 0 级台阶往上爬,f(0) 等于 cost[0];如果 i 等于 1,也可以直接从第 1 级台阶往上爬,f(1) 等于 cost[1]

二、递归代码

状态转移方程其实是一个递归的表达式,可以很方便地将它转换成递归代码,如下所示:

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

在上述代码中,递归函数 f 和状态转移方程相对应,根据从第 i - 1 级台阶和第 i - 2 级台阶往上爬的最少成本求从第 i 级台阶往上爬的最少成本。

上述代码看起来很简捷,但时间效率非常糟糕。时间效率是面试官非常关心的问题,如果应聘者的解法的时间效率糟糕则很难通过面试。根据前面的递归代码,为了求得 f(i) 需先求得 f(i - 1) 和 f(i - 2)。如果将求解过程用一个树形结构表示(如下图中求解 (9) 的过程),就能发现在求解过程中有很多重复的节点

求解 f(i) 这个问题的解,依赖于求解 f(i - 1) 和 f(i - 2) 这两个子问题的解,由于求解 f(i - 1) 和 f(i - 2) 这两个子问题有重叠的部分,如果只是简单地将状态转移方程转换成递归的代码就会带来严重的效率问题,因为重复计算是呈指数级增长的

三、使用缓存的递归代码

为了避免重复计算带来的问题,一个常用的解决办法是将已经求解过的问题的结果保存下来。在每次求解一个问题之前,应先检查该问题的求解结果是否已经存在。如果问题的求解结果已经存在,则不再重复计算,只需要从缓存中读取之前求解的结果

class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();vector<int> dp(n, 0);dp[0] = cost[0];  // 考虑 n == 2 的情况helper(cost, dp, n - 1);return min(dp[n - 1], dp[n - 2]);}
private:void helper(vector<int>& cost, vector<int>& dp, int i) {if (i < 2){dp[i] = cost[i];}else if (dp[i] == 0){helper(cost, dp, i - 1);helper(cost, dp, i - 2);dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];}}
};

在上述代码中,数组 dp 用来保存求解每个问题结果的缓存,dp[i] 用来保存 f(i) 的计算结果。该数组的每个元素都初始化为 0。由于题目中从每级台阶往上爬的成本都是正数,因此如果某个问题 f(i) 之前已经求解过,那么 dp[i] 的缓存的结果将是一个大于 0 的数值。只有当 dp[i] 等于 0 时,它对应的 f(i) 之前还没有求解过

有了这个缓存 dp,就能确保每个问题 f(i) 只需求解一次。如果楼梯有 n 级台阶,那么上述代码的时间复杂度是 O(n)。同时,需要一个长度为 n 的数组,因此空间复杂度也是 O(n)。

前面的递归解法都是从大问题入手的,将问题 f(i) 分解成两个子问题 f(i - 1) 和 f(i - 2)。这种从大问题入手的过程是一种自上而下的求解过程。

四、空间复杂度为 O(n) 的迭代代码

也可以自下而上地解决这个过程,也就是从子问题入手,根据两个子问题 f(i - 1) 和 f(i - 2) 的解求出 f(i) 的结果。通常用迭代的代码实现自下而上的求解过程,如下所示:

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

显然,这种解法的时间复杂度和空间复杂度都是 O(n)

五、空间复杂度为 O(1) 的迭代代码

上述迭代代码还能做进一步的优化。前面用一个长度为 n 的数组将所有 f(i) 的结果都保存下来。求解 f(i) 时只需要 f(i - 1) 和 f(i - 2) 的结果,从 f(0) 到 f(i - 3) 的结果其实对求解 f(i) 并没有任何作用。也就是说,在求解每个 f(i) 的时候,需要保存之前的 f(i) 和 f(i - 2) 的结果,因此只要一个长度为 2 的数组即可

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

优化之后的代码的时间复杂度仍然是 O(n),空间复杂度是 O(1)

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

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

相关文章

Linux docker3--数据卷-nginx配置示例

一、因为docker部署服务都是以最小的代价部署&#xff0c;所以通常在容器内部很多依赖和命令无法执行。进入容器修改配置的操作也比较麻烦。本例介绍的数据卷作用就是将容器内的配置和宿主机文件打通&#xff0c;之后修改宿主机的配置文件就相当于修改了docker进程的配置文件&a…

webgl浏览器渲染设置

在浏览器中程序图形化webgl渲染时&#xff0c;有时候发现代码没有问题&#xff0c;但是就是无法渲染或者渲染报错&#xff0c;此时可以尝试如下的设置&#xff1a; 通过在chrome浏览器输入chrome&#xff1a;//flags打开扩展 设置一&#xff08;webgl开发者扩展&#xff09; 设…

复习Day2

AcWing 1214. 波动数列 - AcWing j(n-1)*b与前i-1项的和模n余数相同&#xff1a; 记前i项的和为x,则有 x%nj ,第i项为 -(n-1)b , 前i项的和为 x-(-(n-1)b)即 x(n-1)b , 而 x(n-1)b % n j (n-1)b % n 就是 x(n-1)b 在模n的情况下同余于 x%n (n-1)*b 对于加法,减法,乘法 什么…

2024华为OD统一考试(C卷)最新题库(Java Python C++)

关于华为OD ​ 华为的员工补充途径有三种&#xff0c;分别是校招、OD转正和社招。校招是华为唯一的正式员工入职途径&#xff0c;但是从近几届开始竞争非常激烈&#xff0c;尤其是在CV、AI、NLP等赛道上&#xff0c;所以对于C9等专业的学生来说&#xff0c;可以考虑转向一些冷…

Linux镜像文件下载地址--SCAS 开源镜像站,速度快

SCAS 开源镜像站 https://mirror.iscas.ac.cn/举例&#xff1a; 下载centos7 Index of /centos/7/isos/x86_64/ (iscas.ac.cn)

ARM实验 LED流水灯

.text .global _start _start: 使能GPIOE GPIOF的外设时钟 RCC_MP_AHB4ENSETR的第[4][5]设置为1即可使能GPIOE GPIOF时钟 LDR R0,0X50000A28 指定寄存器地址 LDR R1,[R0] 将寄存器原来的数值读取出来&#xff0c;保存到R1中 ORR R1,R1,#(0x3<<4) 将第4位设置为1 S…

Linux网络协议栈从应用层到内核层②

文章目录 1、bind 源码剖析2、listen 源码剖析3、accept 源码剖析4、connect 源码剖析客户端调用connect成功&#xff0c;但三次握手并未完成&#xff0c;进程是如何阻塞自己客户端在connect时&#xff0c;如何选择源端口客户发送syn封包以及重传服务端收到syn封包&#xff0c;…

算法公式汇总

文章目录 三角函数定义式诱导公式平方关系两角和与差的三角函数积化和差公式和差化积公式倍角公式半角公式万能公式其他公式反三角函数恒等式 三角函数定义式 三角函数 定义式 余切&#xff1a; c o t A 1 t a n A \text { 余切&#xff1a;} \ cotA \frac{1}{tanA} 余切&a…

x-zse-96安卓端纯算,魔改AES还原

两天前发了一个x-zse-96的文章,当时遇到了点问题,只分析到了最后一个白盒AES函数里面,并且当时用dfa攻击还原出了秘钥,IV也确定了,但是加密结果不对,本来打算把下文鸽掉的,因为当时unidbg没跑起来,用frida去hook白盒AES中的每一行汇编有点麻烦,没有unidbg方便.后来小白大佬说un…

内网渗透(一)必须了解Windows工作组

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 1、内网渗透测试简介 内网也叫局域网&#xff0c;是指在…

《自动机理论、语言和计算导论》阅读笔记:p1-p4

《自动机理论、语言和计算导论》学习第1天&#xff0c;p1-p4&#xff0c;总计4页。这只是个人的学习记录&#xff0c;因为很多东西不懂&#xff0c;难免存在理解错误的地方。 一、技术总结 1.有限自动机(finite automata)示例 1.software for checking digital circuits。 …

工作需求,Vue实现登录

加油&#xff0c;新时代打工人&#xff01; vue 2.x Element UI <template><div class"body" :style"{background-image: url(${require(/assets/images/login.png)})}"><el-form :rules"rules" ref"loginForm" :mode…

MySQL 中的索引

MySQL 中的索引 一、索引的创建和删除1.主键会自动添加索引2.unique 约束的字段自动添加索引3.给指定的字段添加索引4.删除指定索引5.查询表上的索引 二、索引的分类三、MySQL索引采用了B树数据结构1.B树的经典面试题 四、其他索引及相关调优1.Hash索引2.聚集索引和非聚集索引3…

【linux线程(四)】初识线程池手撕线程池

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux线程池 1. 前言2. 什么是…

Python 从0开始 一步步基于Django创建项目(3)使用Admin site管理数据模型

本文内容建立在《Python 从0开始 一步步基于Django创建项目&#xff08;2&#xff09;创建应用程序&数据模型》的基础上。 Django提供的admin site&#xff0c;使得网站管理员&#xff0c;能够轻松管理网站的数据模型。 本文首先创建‘管理员账户’&#xff0c;即超级用户…

华为OD机22道试题

华为OD机试题 2.查找小朋友的好朋友位置 在学校中&#xff0c;N 个小朋友站成一队&#xff0c;第 i 个小朋友的身高为 height[i]&#xff0c;第 i 个小朋友可以看到第一个比自己身高更高的小朋友j&#xff0c;那么 j 是 i 的好朋友 (要求&#xff1a;j>i) 。 请重新生成一个…

202305 CSP认证

202305-1 重复局面 第一题直接干 #include<bits/stdc.h> using namespace std; unordered_map<string, int> chess; int main() {ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);string line, str ""; int n;cin >> n;while(n --){str …

数据结构:链式队列

1.设计思想&#xff1a; 我们可以设计出以上五种队列&#xff0c;但是基于时间复杂度&#xff0c;和空间复杂度的最优解&#xff0c;我们选择入队和出队均为O(1)的&#xff0c;也就是第五种 2.结构设计 typedef struct LPNode//数据节点 {int data;//数据struct LPNode* next…

Redis消息队列与thinkphp/queue操作

业务场景 场景一 用户完成注册后需要发送欢迎注册的问候邮件、同时后台要发送实时消息给用户对应的业务员有新的客户注册、最后将用户的注册数据通过接口推送到一个营销用的第三方平台。 遇到两个问题&#xff1a; 由于代码是串行方式&#xff0c;流程大致为&#xff1a;开…

基于STC12C5A60S2系列1T 8051单片机可编程计数阵列CCP/PCA/PWM模块的捕获模式(外部中断)应用

基于STC12C5A60S2系列1T 8051单片机可编程计数阵列CCP/PCA/PWM模块的捕获模式(外部中断)应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍STC12C5A60S2系列1T 805…