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

news2024/11/15 15:39:47

目录

前言

面试题 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.coloradmin.cn/o/1539059.html

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

相关文章

C++ —— 日期计算器

1. 头文件 #pragma once #include <iostream> using namespace std;class Date { public:Date(int year 1, int month 1, int day 1);int GetMonthDay();bool operator>(const Date& d) const;bool operator>(const Date& d)const;bool operator<(c…

机器学习--jupyter-matplotlib使用中无法显示中文

jupyter使用中无法显示中文 在jupyter中&#xff0c;通过matplotlib作图时可能会添加中文标题&#xff0c;但有时候会不显示中文 import numpy as np import matplotlib.pyplot as pltx np.arange(0, 6, 0.1) # 以0.1为单位&#xff0c;成0到6的数据 y1 np.sin(x) y2 np.c…

ubuntu安装多个gcc并设置可切换

测试环境&#xff1a; Ubuntu16.04 1. 查看当前有几个gcc&#xff0c;g ls /usr/bin/gcc* ls /usr/bin/g* 有两个版本&#xff0c;5和7. 2. 安装特定gcc/g 版本 可以用sudo apt install gcc-version安装&#xff0c;比如说我想安装gcc-7&#xff0c;则命令为sudo apt instal…

第5章 数据建模和设计

思维导图 5.1 引言 最常见的6种模式&#xff1a;关系模式、多维模式、面向对象模式、 事实模式、时间序列模式和NoSQL模式 每种模式分为三层模型&#xff1a;概念模型、逻辑模型和物理模型 每种模型都包含一系列组件&#xff1a;如实体、关系、事实、键和属性。 5.1.1 业务驱…

【Flink】窗口实战:TUMBLE、HOP、SESSION

窗口实战&#xff1a;TUMBLE、HOP、SESSION 1.TUMBLE WINDOW1.1 语法1.2 标识函数1.3 模拟用例 2.HOP WINDOW2.1 语法2.2 标识函数2.3 模拟用例 3.SESSION WINDOW3.1 语法3.2 标识函数3.3 模拟用例 4.更多说明 在流式计算中&#xff0c;流通常是无穷无尽的&#xff0c;我们无法…

C++第十弹---类与对象(七)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、再谈构造函数 1.1、构造函数体赋值 1.2、初始化列表 1.3、explicit关键字 2、static成员 2.1、概念 2.2、特性 2.3、面试题 总结 1、再…

鸿蒙Harmony应用开发—ArkTS(@Prop装饰器:父子单向同步)

Prop装饰的变量可以和父组件建立单向的同步关系。Prop装饰的变量是可变的&#xff0c;但是变化不会同步回其父组件。 说明&#xff1a; 从API version 9开始&#xff0c;该装饰器支持在ArkTS卡片中使用。 概述 Prop装饰的变量和父组件建立单向的同步关系&#xff1a; Prop变量…

leetcode 2617. 网格图中最少访问的格子数【单调栈优化dp+二分】

原题链接&#xff1a;2617. 网格图中最少访问的格子数 题目描述&#xff1a; 给你一个下标从 0 开始的 m x n 整数矩阵 grid 。你一开始的位置在 左上角 格子 (0, 0) 。 当你在格子 (i, j) 的时候&#xff0c;你可以移动到以下格子之一&#xff1a; 满足 j < k < gri…

【单元测试】一文读懂java单元测试

目录 1. 什么是单元测试2. 为什么要单元测试3. 单元测试框架 - JUnit3.1 JUnit 简介3.2 JUnit 内容3.3 JUnit 使用3.3.1 Controller 层单元测试3.3.2 Service 层单元测试3.3.3 Dao 层单元测试3.3.4 异常测试3.3.5 测试套件测多个类3.3.6 idea 中查看单元测试覆盖率3.3.7 JUnit …

Excel使用VLOOKUP函数

VLOOKUP(lookup_value,table_array,col_index_num,range_lookup) 释义&#xff1a; lookup_value&#xff1a;要查找的值&#xff0c;包括数字&#xff0c;文本等 table_array&#xff1a;要查找的值以及预期返回的内容所在的区域 col_index_num&#xff1a;查找的区域的列…

安装mysql8.0.36遇到的问题没有developer default 选项问题

安装mysql8.0.36的话没有developer default选项&#xff0c;直接选择customer就好了&#xff0c;点击next之后通过点击左边Available Products里面的号和中间一列的右箭头添加要安装的产品&#xff0c;最后会剩下6个 安装完成后默认是启动了&#xff0c;并且在电脑注册表注册了…

机器学习——决策树剪枝算法

机器学习——决策树剪枝算法 决策树是一种常用的机器学习模型&#xff0c;它能够根据数据特征的不同进行分类或回归。在决策树的构建过程中&#xff0c;剪枝算法是为了防止过拟合&#xff0c;提高模型的泛化能力而提出的重要技术。本篇博客将介绍剪枝处理的概念、预剪枝和后剪…

《优化接口设计的思路》系列:第九篇—用好缓存,让你的接口速度飞起来

一、前言 大家好&#xff01;我是sum墨&#xff0c;一个一线的底层码农&#xff0c;平时喜欢研究和思考一些技术相关的问题并整理成文&#xff0c;限于本人水平&#xff0c;如果文章和代码有表述不当之处&#xff0c;还请不吝赐教。 作为一名从业已达六年的老码农&#xff0c…

vue2 自定义 v-model (model选项的使用)

效果预览 model 选项的语法 每个组件上只能有一个 v-model。v-model 默认会占用名为 value 的 prop 和名为 input 的事件&#xff0c;即 model 选项的默认值为 model: {prop: "value",event: "input",},通过修改 model 选项&#xff0c;即可自定义v-model …

35 跨域相关问题, 以及常见的解决方式

前言 跨域相关 这是一个 经常会碰到的问题 然后 常见的解决方式 也大概就是几种, 各有各的问题 这里仅仅是 从理论上 来探讨这个问题 主流的解决方式 是通过代理, 将不同域 合并到同一个域 测试用例 测试用例如下, 这里仅仅是一个简单的数据展示 获取对方 “/config.jso…

【c++入门】引用,内联函数,auto

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本节我们来到c中一个重要的部分&#xff1a;引用 目录 1.引用的基本概念与用法1.1引用特性1.2使用场景1.3传值、传引用效率比较1.4引用做返回值1.5引用和指针的对…

Kubernetes(k8s)集群健康检查常用的五种指标

文章目录 1、节点健康指标2、Pod健康指标3、服务健康指标4、网络健康指标5、存储健康指标 1、节点健康指标 节点状态&#xff1a;检查节点是否处于Ready状态&#xff0c;以及是否存在任何异常状态。 资源利用率&#xff1a;监控节点的CPU、内存、磁盘等资源的使用情况&#xf…

SpringCloud从入门到精通速成(二)

文章目录 1.Nacos配置管理1.1.统一配置管理1.1.1.在nacos中添加配置文件1.1.2.从微服务拉取配置 1.2.配置热更新1.2.1.方式一1.2.2.方式二 1.3.配置共享1&#xff09;添加一个环境共享配置2&#xff09;在user-service中读取共享配置3&#xff09;运行两个UserApplication&…

c语言食堂就餐排队问题290行

定制魏&#xff1a;QTWZPW&#xff0c;获取更多源码等 目录 题目 数据结构 函数设计 结构设计 总结 效果截图 ​ 主函数代码 题目 设计一个程序来模拟食堂就餐排队问题&#xff0c;通过输入学生人数和面包数量&#xff0c;计算有多少学生能够吃到午餐。 数据结构 该…

原神x星穹铁道文本转原神语音源码

《原神》x《星穹铁道》文本转原神语音源码介绍文案 探索未知的奇幻世界&#xff0c;与心仪的角色共舞冒险之旅——《原神》与《星穹铁道》的梦幻联动&#xff0c;为你带来前所未有的游戏体验&#xff01;而此刻&#xff0c;我们将为你揭秘一项革命性的创新&#xff1a;文本转原…