算法学习笔记(8.2)-动态规划入门进阶

目录

问题判断:

问题求解步骤:

图例:

解析:

方法一:暴力搜索

实现代码如下所示:

解析:

方法二:记忆化搜索

代码示例:

解析:

方法三:动态规划

空间优化

代码如下

问题判断:

总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常适合用动态规划求解。然而,我们很难从问题的描述中直接提取出这些特征,因此通常需要放宽条件,首先需要观察问题适不适合使用回溯(穷举)解决。

适合用回溯解决的问题通常满足“决策树模型”,对于问题可以使用树形结构来描述,其中每一个节点代表一个决策,每一条路径代表一个决策序列。

换句话说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型,通常可以使用回溯解决。

在此基础上,动态规划问题还有一些判断的“加分项”。

1. 问题包含最大(小)或最多(少)等优化描述。

2. 问题的状态能够使用一个列表、多维矩阵或树来表示,并且一个状态与其周围的状态存在递推关系。

相应地,也存在一些“减分项”。

1. 问题的目标是找出所有的解决方案,而不是找出最优解。

2. 问题描述中有明显的排列组合特征,需要返回具体的对个方案。

如果一个问题满足决策树模型,并具有较为明显的“加分项”,我们就可以假设它是一个动态规划问题,并在求解过程中验证它。

问题求解步骤:

动态规划的问题解题流程会因为问题的性质和难度有所不同,但通常遵循一下步骤:描述决策,定义状态,建立dp表,推导状态转移方程,确定边界问题等。

为了理解动态规划的解题的过程,使用一个经典的例题“最短路径和”

Question

给定一个n * m的二维网格grid,网格中的每个单元包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回左上角到右下角的最小路径和。

图例:

给定网格的最小路径和为13

第一步:思考每轮的决策,定义状态,从而得到dp表

本题的每一轮的决策就是从当前格子向下或者向右走一步。设当前格的行列索引为[i,j],则向下或向右走一步后,索引变为[i+1,j]或[i,j+1]。因此,状态应包含行索引和列索引两个变量,记为[i,j]。

状态[i,j]对应的子问题为:从起始点[0,0]走到[i,j]的最小路径和,记作dp[i,j]。

如图所示:dp二维矩阵,其尺寸与输入网格grid相同。

注意点:(Note)

动态规划和回溯过程可以描述成为一个决策序列,而状态由所有决策变量构成。应当包含描述解题进度的所有变量,其包含了足够的信息,能用来推导下一个状态。

每个状态都对应一个子问题,我们会定义成为一个dp表来存储所有子问题的解,状态的每个独立变量都是dp表中的一个维度。从本质上看,dp表是状态和子问题的解之间的映射。

第二步:找出最优子结构,进而推导出状态转移方程

对应状态[i,j],它只能从上边的格子[i-1,j]和左边的格子[I,j-1]转移过来。因此最优子结构为:到达[i,j]的最小路径和由[i-1,j]的最小路径和与[I,j-1的最小路径和哪个较小来决定。

根据以上的分析得出状态转移方程为:

dp[i,j] = min(dp[i-1,j],dp[i,j-1]) +grid[i,j]

图示如下:

注意点:(Note)

根据定义好的dp表,思考原问题和子问题的关系,找出通过子问题的最优解来构造原问题的最优解的方法,即最优子结构。

一旦我们找到了最优子结构,就可以使用它来构建出来状态转移方程。

第三步:确定边界条件和状态转移顺序

在本题中,处在首行的状态只能从其左边的状态得来,处在首列的状态只能从其上边的状态得来,因此首行i = 0 和首列的 j = 0 是边界条件。

如图所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用循环来遍历矩阵,外循环遍历各行,内循环遍历各列。

解析:

根据对上面的理解我们已经可以写出动态规划的代码。然而子问题的解决是一种从顶至底的思想,因此按照“暴力搜索”->“记忆化搜索”->“动态规划”的顺序实现符合思维习惯。

方法一:暴力搜索

从状态[i,j]开始搜索,不断分解为更小的状态[i-1,j]和[i,j-1],递归函数包括以下的要素。

  1. 递归参数:状态[i,j]。
  2. 返回值:从[0,0]到[i,j]的最小路径和dp[i,j]。
  3. 终止条件:当 i = 0 且 j = 0 时,返回代价grid[0,0]。
  4. 剪枝:当i<0时或j<0时索引越界时,此时返回代价+∞,代表不可行。
实现代码如下所示
# python 代码示例
def min_path_sum_dfs(grid, i, j) :if i == 0 and j == 0 :return grid[0][0]if i < 0 or j < 0 :return infup = min_path_sum_dfs(grid, i - 1, j)left = min_path_sum_dfs(grid, i, j - 1)return min(up, left) + grid[i][j]
// c++ 代码示例
int minPathSumDFS(vector<vector<int>> &grid, int i, int j)
{if (i == 0 && j == 0){return grid[0][0] ;}    if (i < 0 or j < 0){retunr INT_MAX ;}int up = minPathSumDFS(grid, i - 1, j) ;int left = minPathSumDFS(grid, i, j - 1) ;return min(up, left) + gird[i][j] ;
}

解析:

给出一个dp[2,1]为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格grid的尺寸变大而急剧增多。

造成重叠子问题的原因:存在多条路径可以从左上角到达某一单元格。

每个状态都有向下和向右两种选择,从左上角走到右下角总共需要m+n-2步,所以最差的时间复杂度为O(2^(m+n))。这种计算方法并没有考虑网格的边界情况,当到达网格边界时只剩下一种选择,因此实际的路径会少一些。

方法二:记忆化搜索

引入一个与grid网格大小相同的记忆列表mem,用于记录各个子问题的解,并将重叠子问题进行剪枝。

代码示例:
// c++ 代码示例
def min_path_sum_dfs_mem(grid, mem, i, j) :if i == 0 and j == 0 :return grid[0][0]if i < 0 or j < 0 :return infif mem[i][j] != -1 :return mem[i][j]up = min_path_sum_dfs_mem(grid, mem, i - 1, j)left = min_path_sum_dfs_mem(grid, mem, i, j - 1)mem[i][j] = min(up, left) + grid[i][j]return mem[i][j]
// c++ 代码示例
int minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {if (i == 0 && j == 0) {return grid[0][0] ;}if (i < 0 || j < 0) {return INT_MAX ;}if (mem[i][j] != -1) {return mem[i][j] ;}int up = minPathSumDFSMem(grid, mem, i - 1, j) ; int left = minPathSumDFSMem(grid, mem, i, j - 1) ;mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX ;return mem[i][j] ;
}

解析:

在引入记忆化搜索之后,所有的子问题只需要计算一次,因此时间复杂度取决于状态总数,即网格尺寸O(m*n)

方法三:动态规划

基于迭代实现动态规划,代码如下所示:

# pyhton 代码示例
def min_path_sum_dp(grid) :n, m = len(grid), len(grid[0])dp = [ [0] * m for _ in range(n)]dp[0][0] = grid[0][0]for j in range(1, m) :dp[0][j] = dp[0][j - 1] + grid[0][j]for i in range(1, n) :dp[i][0] = dp[i - 1][0] + grid[i][0]for i in range(1, n) :for j in range(1, m) :dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + gird[i][j]return dp[n - 1][m - 1]
int minPathSumDP(vector<vector<int>> &grid) {int n = grid.size(), m = grid[0].size();vector<vector<int>> dp(n, vector<int>(m));dp[0][0] = grid[0][0];for (int j = 1; j < m; j++) {dp[0][j] = dp[0][j - 1] + grid[0][j];}for (int i = 1; i < n; i++) {dp[i][0] = dp[i - 1][0] + grid[i][0];}for (int i = 1; i < n; i++) {for (int j = 1; j < m; j++) {dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];}}return dp[n - 1][m - 1];
}

动态规划的过程如下所示:便利了整个网络,因此时间复杂度为O(m*n)。

数组的dp的大小也为m * n ,因此空间复杂度为O(m*n)。

空间优化

由于每个格子只与左边和上边的格子有关,我们可以只用一个单行数组来实现dp表。

关键:数组dp只能表示一行的状态,我们无法提前初始化首行的状态,而是在遍历每行时更新它。

代码如下:
# python 代码示例def min_path_sum_dp_comp(grid : list[list[int]]) -> int :n, m = len(grid), len(grid[0])dp = [0] * mdp[0] = grid[0][0]for j in range(1, m) :dp[j] = dp[j - 1] + grid[0][j]for i in range(1, n) :dp[0] = dp[0] + grid[i][0]for j in range(1, m) :dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]return dp[m - 1]
// c++ 代码示例
int minPathSumDPComp(vector<vector<int>> &grid)
{int n = grid.size(), m = grid[0].size() ;vector<int> dp(m) ;dp[0] = grid[0][0] ;for (int j = 1 ; j < m ; j++){dp[j] = dp[j - 1] + gird[0][j] ;}for (int i = 1 ; i < n ; i++){dp[0] = dp[0] + grid[i][0] ;for (int j = 1 ; j < m ; j++){dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] ;}}return dp[m- 1] ;
}

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

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

相关文章

每日复盘-20240709

今日关注: 20240709 六日涨幅最大: ------1--------300391--------- 长药控股 五日涨幅最大: ------1--------300391--------- 长药控股 四日涨幅最大: ------1--------603155--------- 新亚强 三日涨幅最大: ------1--------301300--------- 远翔新材 二日涨幅最大: ------1-…

基于antdesign封装一个react的上传组件

项目中遇到了一个上传的需求&#xff0c;看了一下已有的代码很粗糙&#xff0c;而且是直接引用andt的组件&#xff0c;体验不太好&#xff0c;自己使用FormData对象封装了一个上传组件&#xff0c;仅供参考。 代码如下&#xff1a; /*** FileUploadModal* description - 文件选…

Qt入门(二):Qt的基本组件

目录 Designer程序面板 1、布局Layout 打破布局 贴合窗口 2、QWidget的属性 3、Qlabel标签 显示图片 4、QAbstractButton 按钮类 按钮组 5、QLineEdit 单行文本输入框 6、ComboBox 组合框 7、若干与数字相关的组件 Designer程序面板 Qt包含了一个Designer程序 &…

Qt编程技巧总结篇(3)-信号-槽-多线程(二)

文章目录 Qt编程技巧总结篇&#xff08;3&#xff09;-信号-槽-多线程&#xff08;二&#xff09;主进程与子线程线程同步实例与应用 小结 Qt编程技巧总结篇&#xff08;3&#xff09;-信号-槽-多线程&#xff08;二&#xff09; 多线程学习&#xff0c;使用QMutex&#xff0c;…

RTK_ROS_导航(3):点云的压缩,PointCloud转scan

目录 1. 源码的安装2. 修改订阅的话题3. 可视化1. 源码的安装 安装过程如下 mkdir -p point_to_scan_ws/src cd point_to_scan_ws/src git clone https://github.com/BluewhaleRobot/pointcloud_to_laserscan.git cd .. catkin_make source devel/setup.bash2. 修改订阅的话题 …

2024.07.01校招 实习 内推 面经

绿*泡*泡VX&#xff1a; neituijunsir 交流*裙 &#xff0c;内推/实习/校招汇总表格 1、校招 | 元戎启行2025校园招聘正式批正式启动&#xff08;内推&#xff09; 校招 | 元戎启行2025校园招聘正式批正式启动&#xff08;内推&#xff09; 2、提前批 | 多益网络2025届校园…

基于抽象 HandlerInterceptor 快速实现接口鉴权

欢迎关注公众号&#xff1a;冬瓜白 相关文章&#xff1a; 每天学习一点点之 Spring Web MVC 之抽象 HandlerInterceptor 快速实现常用功能&#xff08;限流、权限等&#xff09; 在[每天学习一点点之 Spring Web MVC 之抽象 HandlerInterceptor 快速实现常用功能&#xff08…

Numpy的广播机制(用于自动处理不同形状的数组)

NumPy 广播是一种强大的机制&#xff0c;允许 NumPy 在执行元素级运算时自动处理不同形状的数组。广播的规则使得无需显式地创建匹配形状的数组&#xff0c;直接进行运算&#xff0c;大大简化了代码并提高了效率。 基本概念 广播的基本思想是让较小的数组在需要的维度上进行扩…

【MySQL数据库之概念性问题】

1、关系型数据库和非关系型数据库 关系型数据库&#xff08;Relational Database&#xff0c;简称RDBMS&#xff09;和非关系型数据库&#xff08;NoSQL Database&#xff09;是两种不同的数据库类型。SQL本身叫做结构化查询语言1、关系型数据库&#xff1a;&#xff08;MySQL…

Django 更新数据 save()方法

1&#xff0c;添加模型 Test/app11/models.py from django.db import modelsclass Post(models.Model):title models.CharField(max_length200)content models.TextField()pub_date models.DateTimeField(date published)class Book(models.Model):title models.CharFie…

Spring Boot集成grpc快速入门demo

1.什么是GRPC&#xff1f; gRPC 是一个高性能、开源、通用的RPC框架&#xff0c;由Google推出&#xff0c;基于HTTP2协议标准设计开发&#xff0c;默认采用Protocol Buffers数据序列化协议&#xff0c;支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务&#xff0c…

UE5.3-基础蓝图类整理一

常用蓝图类整理&#xff1a; 1、获取当前关卡名&#xff1a;Get Current LevelName 2、通过关卡名打开关卡&#xff1a;Open Level(by name) 3、碰撞检测事件&#xff1a;Event ActorBeginOverlap 4、获取当前player&#xff1a;Get Player Pawn 5、判断是否相等&#xff1…

深入解析CSS中的!important规则:优先级与最佳实践

先上实践&#xff0c;再讨论设计 在实际工程中&#xff0c;!important 的使用场景通常出现在需要确保某个样式规则具有最高优先级&#xff0c;以覆盖其他可能冲突的样式规则时。以下是一个具体的例子&#xff1a; 场景描述 假设你正在开发一个网站&#xff0c;该网站使用了多…

JavaScript的数组与函数

数组 <script type"text/javascript">/** 知识点&#xff1a;数组* 理解&#xff1a;一维数组的容器* 概念&#xff1a;* 1.数组中的数据叫做元素* 2.元素都有编号叫做下标/索引* 3.下标从0开始* 注意&#xff1a;* 1.数组作为数据的容器…

【JavaScript脚本宇宙】状态管理利器:JavaScript 库全面解析

提升项目效率与可维护性&#xff1a;JavaScript 状态管理库大揭秘 前言 在现代前端开发中&#xff0c;状态管理是一个至关重要的话题。随着复杂性的增加&#xff0c;有效地管理应用程序的状态变得越来越具有挑战性。本文将介绍一些流行的 JavaScript 库&#xff0c;这些库提供…

WEB安全基础:网络安全常用术语

一、攻击类别 漏洞&#xff1a;硬件、软件、协议&#xff0c;代码层次的缺陷。 后⻔&#xff1a;方便后续进行系统留下的隐蔽后⻔程序。 病毒&#xff1a;一种可以自我复制并传播&#xff0c;感染计算机和网络系统的恶意软件(Malware)&#xff0c;它能损害数据、系统功能或拦…

C++语言学习精简笔记(包含C++20特性)

目录 1 C新语法C与CC编译运行String编程范式C基础类型**自动类型推导**统一对象初始化&#xff1a;Uniform Initialization 控制结构if语句for语句switch语句namespace 2 函数函数声明形式参数函数参数传递的选择函数返回值的选择 函数重载 Lambda表达式函数的定义和申明生存期…

磁力猫磁力搜索大全教程,如何使用磁力链接

磁力链接是一种特殊的下载链接&#xff0c;磁力链接可以理解为一个文件识别码&#xff0c;而并非具体的资源地址&#xff0c;下载软件需要拿着这个识别码去整个互联网(DHT网络)去寻找持有该资源的用户(节点)&#xff0c;如果找到则可以进行传输下载。一般年代越久远的磁力链接下…

【一】m2芯片的mac中安装ubuntu24虚拟机集群

文章目录 1. 虚拟机配置2. 复制虚拟机2.1 修改主机名2.2 修改网络 1. 虚拟机配置 在官方网站下载好ubuntu24-arm版镜像开始安装&#xff0c;安装使用VMWare Fusion的社区免费授权版,使用一台m2芯片的mac电脑作为物理机平台。 为什么选择ubuntu24&#xff1f;因为centOS7目前已…

Proteus + Keil单片机仿真教程(五)多位LED数码管的静态显示

Proteus + Keil单片机仿真教程(五)多位LED数码管 上一章节讲解了单个数码管的静态和动态显示,这一章节将对多个数码管的静态显示进行学习,本章节主要难点: 1.锁存器的理解和使用; 2.多个数码管的接线封装方式; 3.Proteus 快速接头的使用。 第一个多位数码管示例 元件…