递归?动态规划?几道题帮你理清楚基本思路!—— 一维动态规划解析

news/2025/3/11 5:15:40/文章来源:https://www.cnblogs.com/Ineedyan/p/18755043

动态规划定义

动态规划(Dynamic Programming,简称 DP)是解决最优化问题的一种重要算法思想。它通过将原问题分解为多个子问题,逐步求解子问题,最终合并子问题的解来解决原问题。动态规划在解决具有重叠子问题和最优子结构性质的问题上非常高效,常用于路径规划、背包问题、序列问题等领域。

单看概念没啥用,了解即可,下面来看一道题,分别从递归、递归优化、动态规划的角度理清解题思路。

Leetcode 70. 爬楼梯假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

Ⅰ 递归思路


接下来先简单的介绍一下递归,已经懂的可以跳过哦~~~
我们可以将递归简单的理解为一个函数不断直接或间接的调用自身,将问题的求解逐渐转换为一个更小的子问题去求解。
在求解相关问题时,我们只关心问题怎么被划分,而不关心子问题如何被解决。因为现在问题的求解和子问题的求解方式是一样的。


举个例子,我们现在想算n!(n的阶乘)的值是多少,顺便理解一下求解递归问题的几个基本要素

① 问题定义(函数定义)

首先,递归需要函数,我们需要明确这个函数是要干什么的,它的功能是什么,要完成一件什么事情。

一般来讲,问题需要求解什么,我们就定义函数代表什么含义,比如,在上面的问题中,我们要求解n的阶乘,那我们就可以定义F(n)代表n的阶乘的值,如下

int F(int n){return n的阶乘;
}

② 递归的关系式(递推式)

在求解阶乘的问题中,可以注意到:

// F(n)= n * (n-1) * (n-2) * (n-3) * ... * 2 * 1;
// 即 F(n) = n * F(n-1), 那么,我们的函数可以这样写:int F(int n){return n * F(n-1);
}

③ 递归结束条件

即递归在什么条件下退出,如果不执行退出,会出现无限递归调用,导致爆栈,即StackOverflow(栈溢出)

在上述的问题中,我们的阶乘算到1的时候就结束了,那么,补充退出条件,完整的代码如下,非常简洁、优雅:

int F(int n){// 结束条件if(n==1){return 1;}return n * F(n-1);
}

OK,想必你已经理解了递归大概的思路,接下来,我们回到爬楼梯的例子。

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

三步走,定义函数,找递归关系式,补充退出条件。
① 题目要求什么我们就定义什么,要求有多少种方法能到某个阶梯,于是我们定义:F(n)表示到达第n个阶梯的方法数量

② 找递归关系式。分析题目,我们可以发现,第n个阶梯有两种到达的方式,从它的下一级走一个台阶上来,或者从它的下两级走两个台阶上来。
那么我们可以得到 F(n) = F(n-1) + F(n-2);

翻译过来就是,我有F(n-2)种方法到达第n-2阶,我有F(n-1)种方法到达第n-1阶,那么我就有F(n-1)+F(n-2)种方法到达第n阶
③ 确定退出条件。根据题目,我们要始终保持函数F(n)的n为正数,也就是说,当n=1或者n=2时,递推就结束了,需要直接返回值。

最终的代码如下:

// climbStairs(n) 表示到达第n级阶梯的方法数public int climbStairs(int n) {// 结束条件, 显然F(1) = 1, F(2) = 2, 当递归到这两个值就直接返回即可if(n == 1){return 1;}if(n == 2){return 2;}// 递归关系式return climbStairs(n-1) + climbStairs(n-2);}

Ⅱ 递归优化策略

虽然递归的代码往往简洁、优雅,但其复杂度是相当高的,比如一道简单难度的爬楼梯,如果直接使用递归的写法,就会得到下面的奖励:

递归的流程如下:

我们可以留意到,在递归的过程中出现了大量的重复计算,如F(8)在左边的递归流程已经计算一次了,但是在右边的递归流程又计算了一遍,导致时间复杂度的增加。

因此,我们可以考虑采用一个数组或者哈希表来存储已经计算过的数值,进行优化,一般我们将这种用于存储已经计算过的结果称作记忆(memory)数组。

优化后的递归代码如下所示:

// 哈希表写法                                                         // 数组写法
class Solution {HashMap<Integer, Integer> memo = new HashMap<>(); // ---------------> int[] memo = new int[n+1];//                  Arrays.fill(memo, -1);public int climbStairs(int n) {// 先检测这个n有没有被计算过,如果计算过,则直接返回其值if(memo.containsKey(n)){                      // ---------------> if(memo[n]!=-1){return memo.get(n);                       //                    return memo[n];}                                             //                  }if(n == 1 || n == 2){return n;}// 进行记忆化操作int res = climbStairs(n-1) + climbStairs(n-2);memo.put(n, res);                             // ---------------> memo[n] = res;return res;}
}

优化后,我们就可以愉快的通过这道题啦~


Ⅲ 动态规划解法

在递归的流程图中,我们可以看到,递归包含递和归的两个过程,从F(n) 一路递到 F(1),再从F(1) 一路往上归到 F(n),那我们能不能只保留归的这一个过程呢
可以! 某种意义上来说,拆分问题为子问题,保存中间状态的答案,计算下一个状态的答案,这就是动态规划,其实也有一点“记忆化”的味道在。


因此,在写算法题过程中,很多题其实都可以从(递归/回溯) 优化到 (记忆化递归/记忆化搜索) 优化到 (动态规划/递推)上。


由F(n) = F(n-1) + F(n-2) 这个式子可以发现,我们只需要两个状态,就可以推出下一个状态。
F(1)与F(2)可以推出F(3),F(2)与F(3)可以推出F(4),一路递推下去, F(n)也迎刃而解。

而在动态规划中,最重要的就是递推关系式的确定上。
动态规划的解法代码如下。

public int climbStairs(int n) {if(n == 1 || n == 2){return n;}int f1 = 1;  // 存放F(n-2)int f2 = 2;  // 存放F(n-1)for(int i=3; i<=n; i++){// F(n) = F(n-2) + F(n-1);int fn = f1 + f2;f1 = f2;f2 = fn;// F(1)   F(2)   F(3)   F(4), ...//  ↑      ↑      ↑//  f1  +  f2  = fn//         ↑      ↑       ↑//         f1  +  f2  =  fn  }return f2;}

我们可以再看一道题加强理解一下。

Leetcode 198. 打家劫舍你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。

刚才提到,动态规划的核心是划分子问题,寻找递推式。这一道题的递推式怎么构思呢?

题目问什么我们就定义函数去解决什么问题,题目描述:给你n间房屋,一夜之间能够偷窃到的最高金额,那我们就定义F(n)代表前n间房屋能够偷窃到的最高金额。

接下来,我们构思递推式。考虑中间状态F(k),即前k间房屋能够偷窃到的最高金额,如何偷k间房子?我们有两种方案:

我们肯定希望能偷到更多的钱,所以最终的结果取两种方案中结果更大的一种,即递推式为:F(k) = Math.max(F(k-2)+nums[i], F(k-1));

你可能会问:在上面的这个图片中,我怎么知道F(k-1)和F(k-2)内部是怎么偷的?但其实,动态规划根本不关心子问题内部是如何被解决的,只关心子问题的划分是否合理!

F(k-1)和F(k-2)也是由他们前面的状态推导得到,而针对于我当下的状态F(k),我只需要知道F(k-1)【前k-1间屋子能偷到多少钱】和F(k-2)【前k-2间屋子能偷到多少钱】,再结合我当下第k间屋子能偷到多少钱,就能推导出最优的方案,从而得到结果。

解法如下:

class Solution {public int rob(int[] nums) {if(nums.length == 1){return nums[0];}if(nums.length == 2){return Math.max(nums[0], nums[1]);}// 存放结果int[] memo = new int[nums.length];memo[0] = nums[0];memo[1] = Math.max(nums[0], nums[1]);for(int i=2; i<nums.length; i++){// 递推关系式memo[i] = Math.max(memo[i-1], memo[i-2]+nums[i]);}return memo[nums.length-1];}
}

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

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

相关文章

ABAP-后台Job相关(转)

定义JOB 查看作业日志 查看JOB变式动态日期变式设置 如果有其他的动态参数设置,建议程序中调用JOBDATA:lv_job_name LIKE tbtco-jobname, "作业名lv_job_nr LIKE tbtco-jobcount, "作业号lv_job_released TYPE c,lv_job_start_so…

在Android Studio上完成一个简单的添加项目(累死了!)

所花时间:7h 代码量(行):430 博客量:5 了解的知识点: 真的累死娃了快 昨天需要完成从web端转向Andriod端的小项目,对于已经明白的来说这是挺容易的(我现在是这样觉得的), 但是昨天什么都不知道,从哪里开始都不了解,只能问AI,一开始是依托于IDEA上进行项目搭建和实…

逆向新手 WriteUp

WriteUp 题目信息 名称:逆向新手.exe 分类:Reverse 描述:找到程序的flag题目链接: https://pan.baidu.com/s/1u8bGbKcUF6_gLaw63L3jyA?pwd=h8r5 提取码: h8r5解题思路 首先用DIE对文件查壳,发现是一个无壳的32位程序。于是直接用32位IDA对文件进行反汇编,得到如下伪代码:…

P3243 [HNOI2015] 菜肴制作(图论)

P3243 [HNOI2015] 菜肴制作 题目描述 知名美食家小 A 被邀请至 ATM 大酒店,为其品评菜肴。ATM 酒店为小 A 准备了 \(n\) 道菜肴,酒店按照为菜肴预估的质量从高到低给予 \(1\) 到 \(n\) 的顺序编号,预估质量最高的菜肴编号为 \(1\)。 由于菜肴之间口味搭配的问题,某些菜肴必…

第一章课后作业

r = float(input("请输入圆的半径:")) area = 3.1415r r print(area)str1 = input("请输入一个人的名字:") str2 = input("请输入一个国家的名字:") print("世界这么大,{}想去{}看看.".format(str1,str2))n = input("请输入整…

Landsat遥感影像分幅条带介绍与矢量下载:WRS的Path与Row

本文介绍Landsat系列卫星的分幅规则,并提供WRS的矢量文件下载~本文介绍Landsat系列卫星的分幅规则,并提供WRS的矢量文件下载。WRS,即Worldwide Reference System,是Landsat系列卫星全球影像标记符号系统,用以区分全球各区域对应的Landsat系列卫星影像编号;其用“Path”与…

Manus爆火,是硬核还是营销?

Manus是一款引发热议的通用Agent产品,凭借强大任务处理能力及营销手段备受关注,其爆火带动了开源复刻潮,并为垂直领域智能体开发提供灵感,如图数据库智能体Chat2Graph。相信这两天小伙伴们应该被Manus刷屏了,铺天盖地的体验解读文章接踵而来,比如「数字生命卡兹克」凌晨爆…

实验一C语言开发环境使用和数据类型、运算符、表达式

实验一 实验任务一 代码点击查看代码 #include<stdio.h> #include<stdlib.h> int main() {printf(" o o\n");printf("<H> <H>\n");printf("I I I I\n");printf(" o \n");printf("<H>\n"…

深度学习(yolo11训练)

这里记录一下用yolo11训练coco数据集。 1、下载coco数据集:https://cocodataset.org/#download,下载这三个文件即可:2017 Train images [118K/18GB],2017 Val images [5K/1GB],2017 Train/Val annotations [241MB]。 2、使用下面脚本将coco数据集label转为yolo能识别地格式…

项目笔记模板测试2+自动博客图片导入工具

目录如何记录java项目学习博客?大纲模板项目学习与记录模板1. 项目名称2. 日期3. 项目介绍4. TODO 需求分析5. 代码实现6. 功能测试标记说明--------------------------------------------------示例笔记1. 项目名称2. 日期3. 项目介绍4. TODO 需求分析5. 代码实现6. 功能测试…

【学习笔记】SystemVerilog 基本逻辑门电路测试

目录1 基本逻辑门电路测试1.0 仿真激励文件testbench1.1 测试与非门逻辑功能1.2 测试或非门逻辑功能1.3 测试异或门逻辑功能1.4 测试同或门逻辑功能 1 基本逻辑门电路测试 1.0 仿真激励文件testbench 以下测试使用的testbench如下: `timescale 1ns / 1psmodule tb_Lab1_test()…