《剑指 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.mfbz.cn/a/481606.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32 CAN的工作模式

STM32 CAN的工作模式 正常模式 正常模式下就是一个正常的CAN节点&#xff0c;可以向总线发送数据和接收数据。 静默模式 静默模式下&#xff0c;它自己的输出端的逻辑0数据会直接传输到它自己的输入端&#xff0c;逻辑1可以被发送到总线&#xff0c;所以它不能向总线发送显性…

STM32利用串口标准库发送字节,发送数组,发送字符串,发送数字,实现printf功能。

早晨到现在刚刚完成的功能&#xff1a;发送字节&#xff0c;发送数组&#xff0c;发送字符串&#xff0c;发送数字&#xff0c;实现printf功能。 当然这是建立在昨天学习使用串口发送数据的基础上&#xff0c;新建立的功能函数&#xff0c;咱们先来看看这次实验的结果吧&#…

CCDP.02.OS正确部署后的Dashboard摘图说明

前言 在部署成功OpenStack后&#xff0c;应该可以在浏览器打开Dashboard&#xff0c;并对计算资源&#xff08;这里主要是指VM&#xff09;进行管理&#xff0c;也可以在Dashboard上面查看OpenStack是否存在错误&#xff0c;下面&#xff0c;已针对检查的关键点&#xff0c;用红…

程序员表白

啥&#xff1f;&#xff01;你说程序员老实&#xff0c;认真工作&#xff0c;根本不会什么表白&#xff01;那你就错了&#xff01;(除了我) 那今天我们就来讲一下这几个代码&#xff01;赶紧复制下来&#xff0c;这些代码肯定有你有用的时候&#xff01; 1.Python爱心代码 im…

IAB欧洲发布首张泛欧洲数字零售媒体能力矩阵图

2024年1月18日&#xff0c;互动广告署-欧洲办事处&#xff08;IAB Europe)发布了首张泛欧洲数字零售媒体能力矩阵图。为媒体买家提供的新资源概述了在欧洲运营的零售商提供的现场、场外和数字店内零售媒体广告机会。 2024年1月18日&#xff0c;比利时布鲁塞尔&#xff0c;欧洲领…

算法系列--递归(2)

&#x1f495;"什么样的灵魂就要什么样的养料&#xff0c;越悲怆的时候我越想嬉皮。"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;算法系列–递归(2) 前言:今天带来的是算法系列--递归(2)的讲解,包含六个和二叉树相关的题目哦 1.计算布尔⼆叉树的…

企业微信可以更换公司主体吗?

企业微信变更主体有什么作用&#xff1f;当我们的企业因为各种原因需要注销或已经注销&#xff0c;或者运营变更等情况&#xff0c;企业微信无法继续使用原主体继续使用时&#xff0c;可以申请企业主体变更&#xff0c;变更为新的主体。企业微信变更主体的条件有哪些&#xff1…

Ambari+Metrics+Bigtop 全家桶编译部署攻略——Ambari系列

您的支持是我继续创作与分享的动力源泉!!! 您的支持是我继续创作与分享的动力源泉!!! 您的支持是我继续创作与分享的动力源泉!!! 写在前面: 1、源码已经完成Ambari+Metrics+Bigtop 最新版的编译及部署,后续会放魔改包和一件部署脚本 2、时间有限,我会尽快更新完毕所有内容…

python 空间距离计算

目录 python 空间距离计算 已知两点&#xff0c;画三角形 批量矩阵计算 python 空间距离计算 要在空间中找到一个点&#xff0c;使其位于点 b 和 c 之间的连线上&#xff0c;并且与点 b 的距离等于点 a 到点 b 的距离的2倍。 import numpy as npif __name__ __main__:a …

【机器学习入门 】逻辑斯蒂回归和分类

系列文章目录 第1章 专家系统 第2章 决策树 第3章 神经元和感知机 识别手写数字——感知机 第4章 线性回归 文章目录 系列文章目录前言一、分类问题的数学形式二、最大似然估计三、交叉熵损失函数四、多类别分类多类别逻辑斯蒂回归归一化指数函数交叉熵误差和均方误差的比较 五…

docker (一)

1&#xff0c;什么是docker 首先我们可以好好的看看docker的那个可能的图标&#xff0c;你想象到了什么&#xff1f; ... docker是一个开源的应用容器引擎&#xff0c;有Docker公司&#xff08;前dotCloud公司&#xff09;开发&#xff0c;基于Apache2.0开源授权协议发行。该…

网络工程师笔记15(OSPF协议-2)

OSPF协议 OSPF是典型的链路状态路由协议&#xff0c;是目前业内使用非常广泛的 IGP 协议之一。 Router-ID(Router ldentifier&#xff0c;路由器标识符)&#xff0c;用于在一个 OSPF 域中唯一地标识一台路由器。Router-ID 的设定可以通过手工配置的方式&#xff0c;或使用系统自…

吴恩达机器学习笔记 二十七 决策树中连续值特征的选择 回归树

还是猫狗分类的案例&#xff0c;假如再增加一个特征weight&#xff0c;该值是一个连续的值&#xff0c;如何在决策树中使用该特征&#xff1f; 如下图所示&#xff0c;尝试不同的阈值&#xff0c;如 weight<9 , 此时左边有四个样本&#xff0c;都为猫&#xff0c;右边有六个…

c语言--内存函数的使用(memcpy、memcmp、memset、memmove)

目录 一、memcpy()1.1声明1.2参数1.3返回值1.4memcpy的使用1.5memcpy模拟使用1.6注意 二、memmove()2.1声明2.2参数2.3返回值2.4使用2.5memmove&#xff08;&#xff09;模拟实现 三、memset3.1声明3.2参数3.3返回值3.4使用 四、memcmp()4.1声明4.2参数4.3返回值4.4使用 五、注…

Android视角看鸿蒙第八课(module.json5中的各字段含义之abilities)下

Android视角看鸿蒙第八课(module.json5中的各字段含义之abilities&#xff09;下 导读 上篇文章开始学习abilities下的各字段含义&#xff0c;因为篇幅原因只学习了name、srcEntry、description、icon和label字段的含义和用法&#xff0c; 这篇文章继续学习和了解其他字段。 …

二叉树|106.从中序与后序遍历序列构造二叉树

力扣题目链接 class Solution { private:TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {if (postorder.size() 0) return NULL;// 后序遍历数组最后一个元素&#xff0c;就是当前的中间节点int rootValue postorder[postorder.…

二分查找法总结

目录 1、思路讲解&#xff08;LC704&#xff09;2、代码思路讲解&#xff08;循环不变量&#xff09;&#xff08;1&#xff09; 左闭右闭&#xff08;2&#xff09;左闭右开&#xff08;3&#xff09;总结&#xff1a;左开右闭和左闭右开&#xff08;4&#xff09;复杂度分析 …

TCP和UDP 传输层协议的区别

TCP协议 当一台计算机想要与另一台计算机通讯时&#xff0c;两台计算机之间的通信需要畅通且可靠&#xff0c;这样才能保证正确收发数据。例如&#xff0c;当你想查看网页或查看电子邮件时&#xff0c;希望完整且按顺序查看网页&#xff0c;而不丢失任何内容。当你下载文件时&…

Docker学习笔记 - 基本概念

一. 什么是“容器”&#xff08;container&#xff09;和“镜像”&#xff08;Image&#xff09; 所谓“容器”可以理解为一个模拟操作系统的虚拟层&#xff0c;大部分是基于Linux的&#xff0c;应用程序及其配置信息&#xff0c;依赖库可以打包成一个Image独立运行在这个虚拟…

nvidia显卡如何安装cuda驱动

目录 查看显卡对应的cuda版本下载与你显卡匹配的CUDA Toolkit 查看显卡对应的cuda版本 按 微软 R 键&#xff0c;输入cmd 然后输入 nvidia-smi &#xff0c;回车显示下面信息&#xff1a; 看到 CUDA Version 为 12.2 下载与你显卡匹配的CUDA Toolkit 打开网页&#xff1a…
最新文章