当前位置: 首页 > news >正文

算法——动态规划:基础

文章目录

  • 一、基本介绍
  • 二、案例——斐波那契数列
    • 1. 基本介绍
    • 2. 递归实现
    • 3. 动态规划
      • 3.1 重叠子问题
      • 3.2 最优子结构
      • 3.3 无后效性
      • 3.4 性质的总结
    • 4. 使用 动态规划 的思想实现
      • 4.1 自顶向下 的 递归
      • 4.2 自底向上 的 递推
      • 4.3 两种思路的简单比较
  • 三、总结


一、基本介绍

动态规划(Dynamic Programming,DP)是求解 多阶段决策 问题 最优化 的一种算法思想,它将 大问题分解成更简单的子问题,对 整体问题的最优解 取决于 子问题的最优解。用于解决具有 重叠子问题最优子结构 特征的问题。关于 重叠子问题最优子结构,在案例中会进行介绍。

二、案例——斐波那契数列

1. 基本介绍

斐波那契数列 是一个 递推数列,它的每个数字是前面两个数字之和,如 1 , 1 , 2 , 3 , 5 , 8 ⋯ 1, 1, 2, 3, 5, 8 \cdots 1,1,2,3,5,8。其递推公式为 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n - 1) + f(n - 2) f(n)=f(n1)+f(n2) n ≥ 3 n \geq 3 n3 n ∈ N + n \in N_+ nN+,此外 f ( 1 ) = 1 , f ( 2 ) = 1 f(1) = 1, f(2) = 1 f(1)=1,f(2)=1。总结就是如下的公式:
f ( n ) = { 1 , n = 1 , 2 f ( n − 1 ) + f ( n − 2 ) , n ≥ 3 , n ∈ N + f(n) = \begin{cases} 1, n = 1, 2 \\ f(n - 1) + f(n - 2), n \geq 3 \end{cases} , n \in N_+ f(n)={1,n=1,2f(n1)+f(n2),n3,nN+

2. 递归实现

递归的实现很简单,只看递推公式就可以了。代码如下:

int f(int n) {if (n == 1 || n == 2) {return 1;}return f(n - 1) + f(n - 2);
}

可以发现,每计算一个总问题 f ( n ) f(n) f(n),就需要计算两个子问题 f ( n − 1 ) , f ( n − 2 ) f(n - 1), f(n - 2) f(n1),f(n2),从而导致该实现的时间复杂度为 O ( 2 n ) O(2^n) O(2n),算是非常差劲的时间复杂度了,这时就需要考虑使用 动态规划 进行优化。

3. 动态规划

前面提到过,使用 动态规划 解决的问题有两个特征。

3.1 重叠子问题

重叠子问题 有两个特点:

  • 子问题是原大问题的小版本,计算步骤完全一样
  • 计算大问题时,需要多次 重复 计算小问题。

在斐波那契数列中,计算 f ( 5 ) f(5) f(5) 会被分解成以下的子问题:
alt text
可以发现, f ( 3 ) f(3) f(3) 被计算了多次,实际上只需要计算一次,保存其结果即可。

一个子问题的多次重复计算会耗费大量时间,这里就能想到一种策略——以空间换时间:保存已经计算过的子问题的结果。在之后的使用时,直接用保存的结果进行计算。这就是 动态规划 的思想,避免重复计算某个子问题,但会耗费更多的空间

使用这种策略,就可以将计算 f ( 5 ) f(5) f(5) 优化成以下的子问题:
alt text

3.2 最优子结构

最优子结构 也有两个特点:

  • 大问题的最优解 包含 小问题的最优解。
  • 可以通过小问题的最优解 推导出 大问题的最优解。

在斐波那契数列中,要计算 f ( n ) f(n) f(n),就得先计算 f ( n − 1 ) , f ( n − 2 ) f(n - 1), f(n - 2) f(n1),f(n2)。其中, f ( n ) f(n) f(n) 是大问题的最优解, f ( n − 1 ) , f ( n − 2 ) f(n - 1), f(n - 2) f(n1),f(n2) 是小问题的最优解。由于斐波那契数列比较简单,每个 n n n 只对应一个解,所以没有体现出 最优 的概念,之后的各种背包问题会有 最优 的概念。

3.3 无后效性

动态规划 中还有一个名词:无后效性,它也比较重要。

无后效性 是使用 动态规划必要条件,如果问题不满足 无后效性,则无法使用动态规划。无后效性 意味着 只关心结果,不关心过程,即只需要保存计算的结果,不需要保存计算的过程。

在斐波那契数列中,要计算 f ( n ) f(n) f(n),就得先知道 f ( n − 1 ) , f ( n − 2 ) f(n - 1), f(n - 2) f(n1),f(n2) 的结果,而不需要知道它们是如何计算的。

3.4 性质的总结

最优子结构 所具有的性质(可以通过小问题的最优解 推导出 大问题的最优解)足以说明该问题有 无后效性。只要一个问题具有 重叠子问题最优子结构 的特征,就可以使用 动态规划 的思想进行优化。

4. 使用 动态规划 的思想实现

动态规划 有两种编程思路:

  • 自顶向下(Top-Down):先大问题,再小问题。通常采用 递归 实现,也叫 记忆化搜索
  • 自底向上(Bottom-Up):先小问题,再大问题。通常采用 递推 实现。

说明:此处的“先”不代表“先解决”,而是代表“先遍历到”,无论哪种实现方式,都是先解决小问题,然后才能解决大问题,并且所有问题都只被解决一次。此外,这两种思路的时间复杂度和空间复杂度是一样的。

4.1 自顶向下 的 递归

  • 中心思想:先 考虑 大问题,再缩小到小问题,直到最小的问题,最后从小到大依次解决所有问题
  • 实现:解决完子问题后就将其结果保存起来,如果需要再次使用,直接获取保存的结果。

使用 递归 的 动态规划 的思想,实现的 斐波那契数列 如下所示:

const int N = 255; // N 是一个常数,表示数组的长度
int memoize[N]; // 用于保存结果的数组
int f(int n) {if (n == 1 || n == 2) {return 1;}if (memoize[n] != 0) { // 如果数组中保存了 f(n)return memoize[n]; // 则直接将其返回}// 否则计算出 f(n),先保存它,然后再返回memoize[n] = f(n - 1) + f(n - 2);return memoize[n];
}

这种方式的时间复杂度为 O ( n ) O(n) O(n)

4.2 自底向上 的 递推

  • 中心思想:先 解决 子问题,再递推到大问题
  • 实现:使用若干个 for 循环填写一维或多维数组 dpdp 的每个元素都有具体含义。
  • 注意:递推 避免了多层递归导致的 栈溢出 问题,使用动态规划时一般采用这种思路。

使用 递推 的 动态规划 的思想,实现的 斐波那契数列 如下所示:

const int N = 255; // N 是一个常数,表示数组的长度
int dp[N]; // dp[i] 表示 f(i)
int f(int n) {dp[1] = dp[2] = 1; // 初始状态for (int i = 3; i <= n; i++) {dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];
}

这种方式的时间复杂度也为 O ( n ) O(n) O(n)

4.3 两种思路的简单比较

  • 自顶向下 的 递归:能够更宏观地把握问题、认清问题的本质。
  • 自底向上 的 递推:编码更直接,不会造成 栈溢出 问题。

三、总结

动态规划 经常用于具有 重叠子问题最优子结构 特征的问题,使用 空间 换 时间 的思想,有 递归递推 两种实现方式,一般使用 递推 的方式,所以经常在动态规划的题解中看到 dp 数组。

本文介绍了动态规划中最基础的知识,更高级的知识(如滚动数组、背包问题等)将会在之后的文章中依次展开。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 基于Android aosp系统的云手机chromium浏览器定制
  • 翻译: 可视化深度学习反向传播原理二
  • CSS技巧专栏:一日一例 20-纯CSS实现点击会凹陷的按钮
  • 前端使用docx-preview展示docx + 后端doc转docx
  • HexView 刷写文件脚本处理工具-基本功能介绍(六)-导出MIME/BIN/FIAT/FORD
  • Android摄像头采集选Camera1还是Camera2?
  • 面试题:Java 集合类的遍历方式,如何一边遍历 一边删除?
  • 电子电气架构 --- 基于AUTOSAR方法论的诊断开发流程
  • 02、MySQL-DML(数据操作语言)
  • k8s 四种Service类型(ClusterIP、NodePort、LoadBalancer、ExternalName)详解
  • 【Android】网络技术知识总结之WebView,HttpURLConnection,OKHttp,XML的pull解析方式
  • 如何高效管理餐厅?餐厅收银系统轻松解决!
  • pgbackrest备份方案(差异和增量备份的区别)
  • 如何在SpringBoot中进行单元测试?
  • Visual Studio Code搭建VUE开发环境
  • 30秒的PHP代码片段(1)数组 - Array
  • Apache Pulsar 2.1 重磅发布
  • download使用浅析
  • JS专题之继承
  • Linux CTF 逆向入门
  • Linux后台研发超实用命令总结
  • nodejs:开发并发布一个nodejs包
  • vuex 笔记整理
  • win10下安装mysql5.7
  • XForms - 更强大的Form
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 给第三方使用接口的 URL 签名实现
  • 基于Javascript, Springboot的管理系统报表查询页面代码设计
  • 码农张的Bug人生 - 初来乍到
  • 前端面试题总结
  • 事件委托的小应用
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • #100天计划# 2013年9月29日
  • #vue3 实现前端下载excel文件模板功能
  • (03)光刻——半导体电路的绘制
  • (delphi11最新学习资料) Object Pascal 学习笔记---第2章第五节(日期和时间)
  • (二)Kafka离线安装 - Zookeeper下载及安装
  • (二十六)Java 数据结构
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • (转)项目管理杂谈-我所期望的新人
  • (转载)CentOS查看系统信息|CentOS查看命令
  • (状压dp)uva 10817 Headmaster's Headache
  • .java 9 找不到符号_java找不到符号
  • .NET Core 中插件式开发实现
  • .NET Core、DNX、DNU、DNVM、MVC6学习资料
  • .NET MAUI Sqlite数据库操作(二)异步初始化方法
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .NET 实现 NTFS 文件系统的硬链接 mklink /J(Junction)
  • .Net(C#)常用转换byte转uint32、byte转float等
  • .NET/C# 将一个命令行参数字符串转换为命令行参数数组 args
  • .NET设计模式(2):单件模式(Singleton Pattern)
  • .net专家(张羿专栏)
  • @FeignClient注解,fallback和fallbackFactory
  • @JsonSerialize注解的使用
  • @media screen 针对不同移动设备