【Hello Algorithm】暴力递归到动态规划(一)
暴力递归到动态规划(一)
- 斐波那契数列的动态规划
- 机器人走路
- 初级递归
- 初级动态规划
- 动态规划
- 先后选牌问题
- 初级递归
- 初级动态规划
- 动态规划
我们可以一句话总结下动态规划
动态规划本质是一种以空间换时间的行为 如果你发现有重复调用的过程 在经过一次之后把结果记下来 下次调用的时候直接用 这就是动态规划
斐波那契数列的动态规划
一般来说我们可以使用递归来解决斐波那契数列问题 代码如下
int fib(int n)
{ if (n <= 0) { cerr << "error" << endl; } if (n <= 2) { return 1; } return fib(n-1) + fib(n-2);
}
当然 这种方式会产生大量的重复计算 所以说我们可以保存上个和上上个的计算值来进行动态规划
int dpfib(int n)
{ if (n <= 2) { return 1; } int i = 1; int j = 1; int k = 0; while (n-2) { k = i + j; i = j; j = k; n--; } return k;
}
这样子写代码就能避免大量的重复计算了
机器人走路
初级递归
假设现在有1~N个位置
有一个小机器人 现在在START位置
它现在要去aim位置 (aim为1~N上的随机一点) 能走K步 (K >= 0)
每次只能走一步 不能越界 不能停止 现在请问有多少种方式能走走到
现在假设 S = 2 N = 5 AIM = 4 K = 6
我们一开始写出的递归方程如下
int process1(int cur , int rest , int aim , int k)
参数含义如下
- cur 当前位置
- rest 剩余步数
- aim 目标地点
- k 能走的步数
base case为
- 如果剩余步数为0 则判断cur是否为aim地点
否则我们执行递归继续往下走
int process1(int cur , int rest , int aim , int n)
{ if (rest == 0){return cur == aim ? 1 : 0;}if (cur == 1){return process1(2 , rest -1 , aim , n);}if (cur == n){return process1(n-1 , rest-1 , aim , n);} return process1(cur-1 , rest-1 , aim , n)+ process1(cur+1 , rest-1 , aim , n);
}
初级动态规划
现在我们要进行进一步的动态规划
我们想想看在递归函数中有真正决定的是哪两个参数
我们每次传递参数的时候 aim
和 n
是不变的
其实每次变化的就是 cur
和 rest
我们可以将该函数往下推演 我们会发现会出现两个相同的结果
如果继续往下展开的话则肯定会有重复的部分 所以说我们最好能将这些函数的结果记录下来 避免重复计算
我们选择使用一个二维数组存储每个函数的计算结果
vector<vector<int>> dp(n + 1 , vector<int>(rest + 1)); for (int i = 0 ; i < n + 1 ; i++ ) { for (int j = 0; j < rest + 1 ; j++) { dp[i][j] = -1; } }
这个二维数组 i j 分别标识当前位置和剩余步数
该数组的值表示在当前位置下走剩余步数能走到目的地有多少种解法
我们首先全部初始化为-1
int _process2(int cur , int rest , int aim , int n , vector<vector<int>>& dp)
{if (dp[cur][rest] != -1){return dp[cur][rest];}int ans = 0;if (rest == 0){ans = cur == aim ? 1 : 0;}else if (cur == 1){ans = _process2(2 , rest-1 , aim , n , dp);}else if (cur == n){ans = _process2(n-1 , rest-1 , aim , n , dp);} else {ans = _process2(cur -1 , rest -1 , aim , n , dp ) + _process2(cur +1 , rest -1 , aim , n , dp); } dp[cur][rest] = ans;return ans;}
之后在我们的函数中 如果数组中有结果 我们就直接返回 如果没有结果我们就将结果记录在数组中后返回 也能得到一样的结果
动态规划
我们以cur为横坐标 rest为纵坐标画一个图 并且将cur为0的时候值填入图中
当cur为1的时候
我们回顾下我们的代码 我们会发现 此时该格上的数字只依赖于 dp[2][rest-1]
当cur为n的时候
我们回顾下之前的带啊吗 我们会发现 此时该格上的数字只依赖于 dp[n-1][rest-1]
当cur介于两者之间的时候
此时该格上的数字依赖于两种路径
dp[cur-1][rest-1] + dp[cur+1][rest-1]
如下图
那么我们既然有了第一列的数字 我们就可以推出整个dp数组的数值
从而我们就能得出 当前为start 还有k步要走的时候 我们有几种路径
代码表示如下
int process3(int cur , int rest , int aim , int n)
{vector<vector<int>> dp(n + 1 , vector<int>(rest + 1)); for (int i = 0 ; i < n + 1 ; i++ ) { for (int j = 0; j < rest + 1 ; j++) { dp[i][j] = 0; } } // row 1 dp[aim][0] = 1; for (int r = 1 ; r <= rest ; r++) { dp[1][r] = dp[2][r-1]; for (int c = 2 ; c < n ; c++) { dp[c][r] = dp[c-1][r-1] + dp[c+1][r-1]; } dp[n][r] = dp[n-1][r-1]; } return dp[cur][rest];
}
这就是完整的解决动态规划问题的步骤
先后选牌问题
初级递归
假设现在给你一个数组 长度为N (N > 0) 数组内部储存着int类型的值 大小为(1~100)
现在两个人先后选数字 有如下规定
- 只能选择最边界的数字
- 如果这个数字被选择了 则从数组中移除
那么我们其实可以写出先选和后选两个函数
一开始我们写出的递归函数如下
int f(vector<int>& arr , int L , int R)
int g(vector<int>& arr , int L , int R)
这里两个函数的意义分别是
- f优先选择的最大值
- g后手选择的最大值
参数的含义是
- arr 数组
- L 左边界
- R 右边界
代码表示如下
int f(vector<int>& arr , int L , int R)
{ if (L == R) { return arr[L]; } int p1 = arr[L] + g(arr , L + 1 , R ); int p2 = arr[R] + g(arr , L , R - 1); return max(p1 , p2);
} int g(vector<int>& arr , int L , int R)
{ if (L == R) { return 0; } int p1 = f(arr , L + 1 , R);int p2 = f(arr , L , R -1);return min(p1 , p2);
}
这里解释下为什么g函数中要取最小值
因为先手的人可能会拿走L或者R 给我们造成p1 或者 p2两种结果
因为先手的人要赢 所以说只可能会给我们最差的一种结果 所以一定会是较小的那个值
初级动态规划
这里变化的参数实际上就只有左边界和右边界
我们就可以围绕着这两个边界来建表
如果按照函数的依赖关系展开 我们很快就会发现重复项
所以说我们要围绕着重复项建表来达到动态规划的效果
而由于这里有两个函数 f 和 g 所以说 我们要建立两张表
我们以为L为横坐标 R为纵坐标建立两张表
L和R的范围是1 ~ R+1
代码表示如下
int _f2(vector<int>& arr , int L , int R , vector<vector<int>>& fmap , vector<vector<int>>& gmap)
{ if (fmap[L][R] != 0) { return fmap[L][R]; } int ans = 0; if ( L == R) { ans = arr[L]; } else { int p1 = arr[L] + _g2(arr , L+1 , R , fmap , gmap); int p2 = arr[R] + _g2(arr , L , R-1 , fmap , gmap); ans = max(p1 , p2); } fmap[L][R] = ans; return ans; }
int _g2(vector<int>& arr , int L , int R , vector<vector<int>>& fmap , vector<vector<int>>& gmap)
{ if (gmap[L][R] != 0){return gmap[L][R];}int ans = 0;if (L == R){ ans = 0;} else { int p1 = _f2(arr , L , R -1 , fmap , gmap);int p2 = _f2(arr , L + 1 , R , fmap , gmap);ans = min(p1 , p2);} gmap[L][R] = ans;return ans;
}
动态规划
我们以L为横坐标 R为纵坐标画一个gmap图 并且将L == R的时候的值填入图中
我们可以发现该图的左下角我们是不需要的因为L不可能大于R
那么我们的gmap上右上角的随机一点是依赖于什么呢?
回归到我们最初的递归方程中
_f2(arr , L , R -1 , fmap , gmap);
_f2(arr , L + 1 , R , fmap , gmap);
我们可以发现是依赖于fmap的 L R-1 以及 L+1 R
如果映射到fmap中 我们可以发现刚好是斜对角线上的两点 既然我们现在知道了斜对角线上的值 我们现在就可以开始填写这两张map表了
代码表示如下
int f3(vector<int>& arr , int L , int R)
{vector<vector<int>> fmap( R + 1, vector<int>(R+1));for (int i = 0 ; i < R + 1 ; i++){for (int j = 0; j < R + 1 ; j++){fmap[i][j] = 0;if (i == j){fmap[i][j] = arr[i];}}}vector<vector<int>> gmap( R+1 , vector<int>(R+1));for (int i = 0 ; i < R + 1 ; i++){for (int j = 0; j < R + 1 ; j++){gmap[i][j] = 0;}} for (int startcol = 1 ; startcol < R + 1; startcol++ ){int col = startcol;int row = 0;while (col < R + 1){fmap[row][col] = max(gmap[row][col-1] + arr[row] , gmap[row+1][col] + arr[col]);gmap[row][col] = min(fmap[row][col-1] , fmap[row+1][col]);row++;col++;}}return fmap[L][R];
}
其实最关键的代码就是这两行
fmap[row][col] = max(gmap[row][col-1] + arr[col] , gmap[row+1][col] + arr[row]);gmap[row][col] = min(fmap[row][col-1] , fmap[row+1][col]);
我们可以发现 这其实就是将原来代码中的函数
int p1 = arr[L] + _g2(arr , L+1 , R , fmap , gmap); int p2 = arr[R] + _g2(arr , L , R-1 , fmap , gmap); ans = max(p1 , p2);
变成了表中的数字相加
这就是动态规划的一般套路
相关文章:

【Hello Algorithm】暴力递归到动态规划(一)
暴力递归到动态规划(一) 斐波那契数列的动态规划机器人走路初级递归初级动态规划动态规划 先后选牌问题初级递归初级动态规划动态规划 我们可以一句话总结下动态规划 动态规划本质是一种以空间换时间的行为 如果你发现有重复调用的过程 在经过一次之后把…...

凉鞋的 Godot 笔记 107. 脚本窗口文件系统窗口
107. 脚本窗口&文件系统窗口 在上一篇,我们完成了第二轮循环,同时也接触了一些新内容,如下所示: 频率使用比较高的窗口,还剩下最后两个了,一个是脚本窗口: 另一个是文件系统窗口: 脚本窗口 和 文件系统…...
数据源作用以及spring配置数据源
数据源 数据源,简单理解为数据源头,提供了应用程序所需要数据的位置。数据源保证了应用程序与目标数据之间交互的规范和协议,它可以是数据库,文件系统等等。其中数据源定义了位置信息,用户验证信息和交互时所需的一些…...
Javaweb中的servlet中的消息体是什么?
2023年10月9日,周一晚上 目录 什么是消息体 什么是HTTP响应 HTTP响应由谁产生,发给谁 响应头具体有什么内容 Content-Type的值怎么写 HTTP响应例子 什么是消息体 消息体(message body)指HTTP响应中的实体主体内容。 什么是HTTP响应 在HTTP响应中…...

饥荒服务器阿里云租用价格表一年和一个月收费报价表
饥荒阿里云服务器多少钱一个月?阿里云服务器价格9元一个月,阿里云轻量应用服务器2核2G3M带宽轻量服务器一年108元,2核4G4M带宽轻量服务器一年297.98元12个月;阿里云ECS云服务器e系列2核2G配置182元一年、2核4G配置365元一年、2核8…...
前端 JS 经典:Math 常用方法汇总
1. Math.ceil 向上取整 Math.ceil(1.2) // 2 2. Math.floor 向下取整 Math.floor(1.2) // 1 3. Math.round 四舍五入 Math.round(1.4) // 1 Math.round(1.6) // 2 4. Math.random 0-1 随机数 Math.random() // 0.2745798547204079 5. Math.max 返回大值 Math.max(1.2,…...

MongoDB 笔记
1 insert 、create、save区别 insert: 主键不存在则正常插入;主键已存在,抛出DuplicateKeyException 异常 save: 主键不存在则正常插入;主键已存在则更新 insertMany:批量插入,等同于批量执行 insert create&#x…...

Maven 项目文档
本章节我们主要学习如何创建 Maven 项目文档。 比如我们在 C:/MVN 目录下,创建了 consumerBanking 项目,Maven 使用下面的命令来快速创建 java 项目: mvn archetype:generate -DgroupIdcom.companyname.bank -DartifactIdconsumerBanking -…...

浏览器中XPath的使用
概念 XPath (XML Path Language) 是一门在 XML 文档中查找信息的语言,可用来在 XML 文档中对元素和属性进行遍历。 XPath定位在爬虫和自动化测试中都比较常用,通过使用路径表达式来选取 XML 文档中的节点或者节点集,熟练掌握XPath可以极大提…...
js录制屏幕并输出视频
借助navigator,需要注意的是navigator.mediaDevices.getDisplayMedia需要在https使用,若部署环境为http,则会导致navigator.mediaDevices.getDisplayMedia为undefined 参数中的name为输出视频的文件名 time为录制的时长,若时长为一秒则time值…...

华为OD机试 - 数组组成的最小数字(Java 2023 B卷 100分)
目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中,刷题点这里 专栏导读 本专栏收录于《华为OD机试(JAVA)真题(A卷B卷&#…...

数据结构-顺序存储二叉树
文章目录 目录 文章目录 前言 一 . 什么是顺序存储二叉树 二 . 模拟实现 前序遍历 总结 前言 大家好,今天给大家讲一下顺序存储二叉树 一 . 什么是顺序存储二叉树 顺序存储二叉树是一种将二叉树的节点按照从上到下、从左到右的顺序存储在数组中的方法。具体来说,顺…...

mysql学习实践
这里写目录标题 查找重复数据查找重复数据的字段值以及重复的次数如果你只想查找重复数据,而不需要知道重复的次数,可以简化查询如下 根据某个字段查询重复的数据,并取id最大的那条数据(用于商机列表展示)将逗号分隔的…...
键盘控制应用--通过键盘发送控制指令
系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言代码原理实现前言 认知有限,望大家多多包涵,有什么问题也希望能够与大家多交流,共同成长! 本文先对键盘控制应用做个简单的介绍,具体内容后…...

python中pytorch的广播机制——Broadcasting
广播机制 numpy 在算术运算期间采用“广播”来处理具有不同形状的 array ,即将较小的阵列在较大的阵列上“广播”,以便它们具有兼容的形状。Broadcasting是一种没有copy数据的expand 不过两个维度不相同,在前面插入维度1扩张维度1到相同的维…...

基于BES平台音乐信号处理之DRC算法实现
基于BES平台音乐信号处理之DRC算法实现 是否需要申请加入数字音频系统研究开发交流答疑群(课题组)?加我微信hezkz17, 本群提供音频技术答疑服务 1 DRC实现 drc.h 2 调用 audio_process.c 3 DRC动态范围控制算法在音乐信号处理中的位置 4 DRC具体细节源码 可参考…...
如何加快香山处理器Chisel->Verilog编译速度
graalvm installation 更换JVM。我们推荐使用GraalVM代替OpenJDK。 使用GraalVM免费版作为JVM编译香山比OpenJDK快10%-20%。 -------------------------------------------------------------------------- https://www.graalvm.org/latest/docs/getting-started/linux/ downl…...

pillow篇---pillow连续打开同一张图片会导致打开失败问题
如果你需要在多次操作同一张图像时避免出现缓存问题,你可以使用 Image.open() 方法的 seek() 方法将文件指针移动到图像数据的开头,以便重新读取图像数据。示例如下: from PIL import Image# 打开图像文件 image Image.open(example.jpg)# …...

详细解说iptables 高阶用法,用来完成哪些高效率网络路由策略场景,iptables 实现域名过滤,Linux如何利用iptables屏蔽某些域名?
详细解说iptables 高阶用法,用来完成哪些高效率网络路由策略场景,iptables 实现域名过滤,Linux如何利用iptables屏蔽某些域名? Linux利用iptables屏蔽某些域名 以下规则是屏蔽以 youtube.com 为主的所有一级 二级 三级等域名。 iptables -A OUTPUT -m string --string &qu…...

面试总结-Redis篇章(十二)——Redis是单线程的,为什么还那么快
Redis是单线程的,为什么还那么快 Redis是单线程的,为什么还那么快什么是IO多路复用 阻塞IO非阻塞IOIO多路复用 Redis是单线程的,为什么还那么快 Redis是纯内存操作,执行速度非常快采用单线程,避免不必要的上下文切换可…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
AGain DB和倍数增益的关系
我在设置一款索尼CMOS芯片时,Again增益0db变化为6DB,画面的变化只有2倍DN的增益,比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析: 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...