动态规划详解(Dynamic Programming)

目录

  • 引入
  • 什么是动态规划?
  • 动态规划的特点
  • 解题办法
  • 解题套路框架
  • 举例说明
    • 斐波那契数列
      • 题目描述
      • 解题思路
        • 方式一:暴力求解
          • 思考
        • 方式二:带备忘录的递归解法
        • 方式三:动态规划
  • 推荐练手题目

引入

动态规划问题(Dynamic Programming)应该是很多人头疼的一类问题,
本文尝试探索一种套路帮助解决此类问题

什么是动态规划?

动态规划的核心思想是将问题分解为一系列子问题,并通过记忆化或递推的方式求解子问题,从而得到原始问题的解。

动态规划的特点

其主要特点包括:

  • 重叠子问题:问题的解能够通过多次重复计算相同的子问题得到。
  • 最优子结构:问题的最优解能够由子问题的最优解推导得出。

解题办法

动态规划通常分为自顶向下的记忆化搜索和自底向上的递推两种方法。

解题套路框架

解决动态规划问题通常遵循以下套路框架:

递归的暴力解法 -> 带备忘录的递归解法 -> 非递归的动态规划解法。这个过程是层层递进的解决问题的过程,

具体的过程

  1. 确定状态
    需要明确问题的状态是什么,以及在状态转移过程中如何更新状态。

  2. 定义状态转移方程
    状态转移方程描述了状态之间的转移关系。它明确了如何从已知的状态计算出未知状态的值。

  3. 处理边界条件
    边界条件是指状态转移的起点或终点,通常需要单独考虑和处理。

  4. 计算动态规划数组
    使用循环遍历计算动态规划数组中的每个元素,根据状态转移方程填充数组。

  5. 返回结果
    根据问题的具体要求,从动态规划数组中选取相应的值作为最终的结果。

举例说明

以下是一个使用动态规划解决斐波那契数列问题的示例:

斐波那契数列

题目描述

斐波那契数列是一个非常经典的数列,它的第一个和第二个数分别为 0 和 1,从第三个数开始,每个数都是前两个数之和。即斐波那契数列的定义如下:

F(0) = 0,
F(1) = 1,
F(n) = F(n-1) + F(n-2) (n >= 2)

给定一个整数 n,编写一个函数来计算斐波那契数列中第 n 个数的值。

例如:

输入:n = 0,输出:0
输入:n = 1,输出:1
输入:n = 5,输出:5
输入:n = 10,输出:55

解题思路

方式一:暴力求解

以java为例

public class Fibonacci {
    // 暴力递归求解斐波那契数列
    public static long fibonacci(int n) {
        // base case
        if (n == 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            // 递归计算前两个数的和
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }

    public static void main(String[] args) {
        int n = 10;
        long result = fibonacci(n);
        System.out.println("The Fibonacci number at position " + n + " is: " + result);
    }
}

说明:
fibonacci方法用于求解第n个斐波那契数,采用递归的方式。
在方法中,首先判断n的值是否为0或1(即基本情况),如果是则返回对应的斐波那契数值。
如果n值大于1,则通过递归计算前两个数的和,即fibonacci(n-1) + fibonacci(n-2)。
在main方法中,我们定义一个整型变量n表示所求的斐波那契数的位置,然后调用fibonacci方法计算结果。
最后,打印出所求位置上的斐波那契数。

思考

学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树。
在这里插入图片描述

递归树是一种用于理解递归算法执行过程的图形表示方法。对于斐波那契数列问题,在递归过程中,我们可以将其表示为一棵递归树。递归树的每个节点表示一个子问题,根节点表示原问题。以计算 f(20) 为例,我们需要先计算子问题 f(19) 和 f(18),然后计算 f(19) 时又需要先计算子问题 f(18) 和 f(17),依此类推。当到达 f(1) 或 f(2) 时,可以直接返回已知的结果,递归树不再向下生长。

递归算法的时间复杂度可以通过计算子问题个数乘以解决一个子问题所需时间来得到。子问题个数指的是递归树中节点的总数。显然,在斐波那契数列的递归树中,节点总数为指数级别,即 O(2^n)。而解决一个子问题的时间,在这个算法中是常数级别的(O(1)),因为只有一个加法操作。

因此,该算法的时间复杂度为 O(2^n),是指数级别的,效率低下。

观察递归树可以明显看出算法低效的原因:存在大量的重复计算。例如,节点 f(18) 被计算了两次。而且不仅仅是节点 f(18),还有其他节点也被重复计算。因此,这个算法非常低效。

动态规划问题具备的第一个性质就是重叠子问题。接下来,我们将想办法解决这个问题。

方式二:带备忘录的递归解法

明确了重复计算是导致算法低效的主要原因后,我们可以采取一种优化策略,即使用一个「备忘录」来避免重复计算。每次计算完一个子问题的答案后,不急于返回,而是将答案存储在「备忘录」中。下次遇到同样的子问题时,先查找「备忘录」,如果已经计算过了,直接取出答案使用,避免重复计算。

通常情况下,我们可以使用数组来充当「备忘录」,但也可以使用哈希表(字典)等其他数据结构,核心思想是一样的。

java实现:

import java.util.HashMap;
import java.util.Map;

public class Fibonacci {
    // 使用备忘录来避免重复计算
    private static Map<Integer, Long> memo = new HashMap<>();

    public static long fibonacci(int n) {
        // 查找备忘录,如果已经计算过,直接返回答案
        if (memo.containsKey(n)) {
            return memo.get(n);
        }

        // Base case
        if (n == 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        }

        // 计算并记录答案到备忘录中
        long ans = fibonacci(n - 1) + fibonacci(n - 2);
        memo.put(n, ans);

        return ans;
    }

    public static void main(String[] args) {
        int n = 20;
        long result = fibonacci(n);
        System.out.println("The Fibonacci number at position " + n + " is: " + result);
    }
}

说明:
我们使用一个哈希表(字典)memo作为「备忘录」,用于存储子问题的答案。

在fibonacci方法中,首先查找「备忘录」,如果已经计算过子问题n的答案,则直接返回答案。这样就避免了重复计算。

接下来,处理基本情况,即斐波那契数列的前两个数。

然后,通过递归计算并记录答案到「备忘录」中,即计算fibonacci(n-1) + fibonacci(n-2)并将结果存入memo。 最后,返回计算结果。

现在,画出递归树,你就知道「备忘录」到底做了什么。
在这里插入图片描述
使用带有备忘录的递归算法时,我们可以将其思路分为以下几个层次进行说明:

  • 第一层次:理解问题存在重复计算导致低效
    我们首先明确问题的低效之处在于重复计算。观察递归树,我们发现存在大量的重复计算,例如节点 f(18) 被计算了两次,这样会浪费大量的时间。

  • 第二层次:引入「备忘录」概念
    为了避免重复计算,我们引入了「备忘录」的概念。在计算完一个子问题的答案后,我们将其结果存储在「备忘录」中。下次遇到相同的子问题时,先查找「备忘录」,如果已经计算过,直接返回结果,从而避免重复计算。

  • 第三层次:优化时间复杂度
    递归算法的时间复杂度可以通过计算子问题个数乘以解决一个子问题所需的时间来得到。对于带有备忘录的递归算法,子问题个数是与输入规模成正比的,因为我们使用备忘录避免了重复计算。解决一个子问题的时间仍然是常数级别。因此,该算法的时间复杂度为 O(n),相比于暴力算法有了显著的降低。

  • 第四层次:对比「自顶向下」和「自底向上」
    我们进一步将带有备忘录的递归解法与动态规划进行对比。带有备忘录的递归解法采用了「自顶向下」的思路,通过递归树从顶部向下延伸,逐步分解规模,直到达到基础情况并逐层返回答案。而动态规划采用了「自底向上」的思路,从最底部、最简单、规模最小的问题开始向上推导,直到得到所需的答案。动态规划通常通过迭代循环实现计算,不使用递归。

通过以上层次的分析进一步理解带有备忘录的递归算法的优势,并将其与动态规划进行对比,有助于深入理解动态规划思想的应用。

方式三:动态规划

java实现

public class Fibonacci {
    public static long fibonacci(int n) {
        // 创建一个数组来存储子问题的解,初始值为0
        long[] dp = new long[n + 1];

        // 初始化前两个数的值
        dp[0] = 0;
        dp[1] = 1;

        // 从第三个数开始迭代计算
        for (int i = 2; i <= n; i++) {
            // 通过状态转移方程计算当前数的值
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        // 返回第n个数的值
        return dp[n];
    }

    public static void main(String[] args) {
        int n = 20;
        long result = fibonacci(n);
        System.out.println("The Fibonacci number at position " + n + " is: " + result);
    }
}

说明:
创建一个数组dp来保存子问题的解,数组的长度为n + 1,初始值都为0。 初始化数组中的前两个数,即dp[0] = 0和dp[1]= 1,这是斐波那契数列的基础情况。 从第三个数开始,使用循环迭代计算每个数的值。通过状态转移方程dp[i] = dp[i - 1] + dp[i - 2],计算当前数的值,即前两个数之和。 最后,返回第n个数的值,即为斐波那契数列中第n个数的结果。```

通过绘制 DP Table,可以更好地理解动态规划解法。而且你会发现,DP Table 实际上与带有备忘录的递归解法中的「备忘录」非常相似,只是顺序相反而已。事实上,当备忘录完成填充后,就形成了这个 DP Table。因此,这两种解法在很大程度上是相似的,而且在大多数情况下,它们的效率也基本相同。

在这里,我们引入了「状态转移方程」这个名词,实际上它描述的是问题结构的数学形式:
F(n) = F(n-1) + F(n-2)
在这里插入图片描述

这个方程告诉我们,要计算第 n 个斐波那契数,我们只需要知道前两个数的值即可。通过这个方程式,我们可以将问题的求解转化为子问题的求解。

「状态转移方程」这个术语可能听起来有些高级,其实它的含义很简单。在斐波那契数列问题中,我们将 f(n) 看作是一个状态 n,而这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来。所以,「状态转移方程」就是描述问题中不同状态之间的变化关系而已。它帮助我们理解问题的结构并找到解决方法。

动态规划的精髓就是找到合适的状态转移方程,它能够将大问题拆分成小问题,并且小问题的结果可以用于求解大问题。在斐波那契数列问题中,状态转移方程适用于计算任意位置的斐波那契数。

因此,通过定义好状态转移方程并利用它,我们可以构建出 DP Table 或使用递归加备忘录的方式来高效地求解斐波那契数列问题。

这只是一个简单的示例,实际的动态规划问题可能会更复杂。但遵循这个解题套路框架,通过确定状态、定义状态转移方程、处理边界条件、计算动态规划数组和返回结果,可以更好地解决各种不同类型的动态规划问题

推荐练手题目

序号题目名称题目描述难度题目链接
1爬楼梯(Climbing Stairs)有n个台阶,每次可以爬1个或2个台阶,求爬到第n个台阶的所有可能的方法数。简单LeetCode 70
2最长递增子序列(Longest Increasing Subsequence)给定一个整数数组,找到其中最长的递增子序列的长度。中等LeetCode 300
3背包问题(Knapsack Problem)有一个背包可以容纳一定重量的物品,给定一组物品的重量和价值,选择一部分物品放入背包,使得选中的物品总重量不超过背包容量,同时总价值最大。中等LeetCode 322
4打家劫舍(House Robber)给定一列房屋,每个房屋中都有一定数量的金额,相邻的房屋不能同时被盗窃。求能够盗窃的最大金额。简单LeetCode 198
5解码方法(Decode Ways)给定一个只包含数字的非空字符串,判断是否有可能解码成字母组合。每个数字转换为对应的字母(A - 1,B - 2,…,Z - 26)。中等LeetCode 91

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

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

相关文章

QT子窗口关闭时自动释放及注意事项

先说方法&#xff0c;很简单&#xff0c;有如下API函数可用&#xff1a; testDialog->setAttribute( Qt::WA_DeleteOnClose, true )&#xff1b; 他的官方解释如下&#xff1a; 最后&#xff0c;说一个注意事项&#xff1a; 最近写python程序比较多&#xff0c;回过头来&a…

OPPO VPC 实践探索

01 概述 一年前(20年6月)&#xff0c;OPPO云网络技术底座开始支持VPC方案&#xff0c;解决了用户担心的云上安全和虚拟实例的性能问题。我们称这个版本为VPC1.0&#xff0c;其采用了先进的智能网卡加速和VXLAN隧道隔离技术&#xff0c;实现了VPC从无到有的突破。 然而由于业务快…

爬虫部署平台crawlab使用说明

Crawlab 是一个基于 Go 语言的分布式网络爬虫管理平台&#xff0c;它支持 Python、Node.js、Jar、EXE 等多种类型的爬虫。 Crawlab 提供了一个可视化的界面&#xff0c;并且可以通过简单的配置来管理和监控爬虫程序。 以下是 Crawlab 的一些主要优点&#xff1a; 集中管理&am…

绿联 安装Mysql数据库

绿联 安装Mysql数据库 1、镜像 mysql:5.7 数据库5.7.x系列。 mysql:8 数据库8.x.x系列&#xff0c;安装方式相同。 2、安装 2.1、拉取镜像 拉取5.7.x版本的镜像。 2.2、基础设置 重启策略&#xff1a;第三或第四项均可。 2.3、网络 桥接即可。 2.4、命令 在原有的“mys…

概率论基础——拉格朗日乘数法

概率论基础——拉格朗日乘数法 概率论是机器学习和优化领域的重要基础之一&#xff0c;而拉格朗日乘数法与KKT条件是解决优化问题中约束条件的重要工具。本文将简单介绍拉格朗日乘数法的基本概念、应用以及如何用Python实现算法。 1. 基本概念 拉格朗日乘数法是一种用来求解…

EPSON机器人仿真实战攻略:从设置通信到运行调试一网打尽!

EPSON机器人 仿真测试深度教程 机器人还没到,怎么提前验证写好得机器人程序? 强大的仿真功能来了!本文详细深入的介绍了仿真的功能,一步步教会你如何仿真! 请先关注公众号收藏,防止走丢! 需要先设置电脑与控制器通信的虚拟连接,设置-电脑与控制器通信-增加-选择连接…

第27篇:T触发器实现4位计数器

Q&#xff1a;本篇我们用T触发器实现时序逻辑电路--计数器。 A&#xff1a;T触发器&#xff08;Toggle Flip-Flop&#xff09;只有一个信号输入端&#xff0c;在时钟有效边沿到来时&#xff0c;输入有效信号则触发器翻转&#xff0c;否则触发器保持不变&#xff0c;因此T触发器…

C++之结构体初始化10种写法总结(二百六十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

大数据毕业设计hadoop+spark旅游推荐系统 旅游可视化系统 地方旅游网站 旅游爬虫 旅游管理系统 计算机毕业设计 机器学习 深度学习 知识图谱

基于hive数据仓库的贵州旅游景点数据分析系统的设计与实现 摘 要 随着旅游业的快速发展和数字化转型&#xff0c;旅游数据的收集和分析变得越来越重要。贵州省作为一个拥有丰富旅游资源的地区&#xff0c;旅游数据的分析对于促进旅游业的发展和提升旅游体验具有重要意义。基…

Redis分布式锁的优化

分布式锁 分布式锁&#xff1a;满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的实现 分布式锁的核心是实现多进程之间互斥&#xff0c;而满足这一点的方式有很多&#xff0c;常见的有三种&#xff1a; MySQLRedisZookeeper互斥利用mysql本身的互斥锁机制利…

LangChain-03 astream_events 流输出

内容简介 尝试用 FAISS 或 DocArrayInMemorySearch 将数据向量化后检索astream_events 的效果为 |H|arrison| worked| at| Kens|ho|.|| 安装依赖 # 之前的依赖即可 pip install --upgrade --quiet langchain-core langchain-community langchain-openai # Win或Linux用户可…

摸鱼toyaml.com更新

摸鱼https://toyaml.com/windowsupdate.html

一次MySQL事务的旅程:Buffer Pool, Binlog, Redo Log揭秘

MySQL中的各种Buffer和Log以及表空间 MySQL中一次事务涉及了各种Buffer,Log和表空间&#xff0c;主要涉及&#xff1a;Buffer Pool, Binlog, Undo Log, Redo Log以及表空间。 我们来探讨下。 Buffer Pool Buffer Pool主要存放在内存中&#xff0c;它是一个缓存区域&#xf…

36---USB HUB电路设计

视频链接 USB HUB电路设计01_哔哩哔哩_bilibili USB HUB 电路设计 1、USB HUB基本介绍 USB Hub&#xff0c;指的是一种可以将一个USB接口扩展为多个&#xff0c;并可以使这些接口同时使用的装置。 Hub也是大家常说的集线器&#xff0c;它使用星型拓扑结构连接多个USB接口设…

【御控物联】JavaScript JSON结构转换(17):数组To对象——键值互换属性重组

文章目录 一、JSON结构转换是什么&#xff1f;二、核心构件之转换映射三、案例之《JSON数组 To JSON对象》四、代码实现五、在线转换工具六、技术资料 一、JSON结构转换是什么&#xff1f; JSON结构转换指的是将一个JSON对象或JSON数组按照一定规则进行重组、筛选、映射或转换…

vue 条件渲染、列表循环渲染、事件绑定 初探第三天

条件渲染 <script>const app Vue.createApp({data(){return {show:true,conditionOne: false,conditionTwo: true,}},template:<div v-if"show"> hello word </div><div v-if"conditionOne"> if </div><div v-else…

HWOD:将字符串中的数字用*括起来

一、知识点 当需要类似括号( )这样成对出现的字符时&#xff0c;可以通过设置flag来标示 比如flag等于0表示前面所有的括号都是成对的 flag等于1表示最靠近的括号是未成对的&#xff1b;满足条件时&#xff0c;补齐括号&#xff0c;使其成对&#xff0c;flag置0 二、题目 …

如何展示科技产品的原理和应用

一、合理安排展示区域 不同的科技产品具有不同的展示需求&#xff0c;设计师需要根据展品的特点和大小&#xff0c;合理安排展示区域。对于较大的科技产品&#xff0c;可以设置特定的展台或展示区域&#xff0c;并配备合适的灯光和装饰&#xff0c;以凸显产品的重要性和独特性。…

matlab实现决策树可视化——信息增益、C4.5、基尼指数

代码&#xff1a;https://download.csdn.net/download/boyas/89074326

第十五章 Nginx

一、Nginx 1.1 Nginx 相关概念 1.1.1 正向代理 正向代理类似一个跳板机&#xff0c;代理访问外部资源。 比如我们国内访问谷歌&#xff0c;直接访问访问不到&#xff0c;我们可以通过一个正向代理服务器&#xff0c;请求发到代理服&#xff0c;代理服务器能够访问谷歌&am…