学妹半夜问我动态规划是咋回事,我啪的一下讲清楚了!
事情是这样的,临近期末考试,学妹突然问我动态规划怎么理解,本着友好互助的原则,不顾女朋友的反对,花了五分钟给她讲清楚,先不说其它的了,你们先看文章,我去跪一会榴莲。
什么是递归
先下定义:递归算法是一种直接或者间接调用自身函数或者方法的算法。
通俗来说,递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。它有如下特点:
- 一个问题的解可以分解为几个子问题的解
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件,即必须有一个明确的递归结束条件,称之为递归出口
通过动画一个一个特点来进行分析。
1.一个问题的解可以分解为几个子问题的解
子问题就是相对与其前面的问题数据规模更小的问题。
在动图中①号问题(一块大区域)划分为②号问题,②号问题由两个子问题(两块中区域)组成。
2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
「①号划分为②号」与「②号划分为③号」的逻辑是一致的,求解思路是一样的。
3. 存在递归终止条件,即存在递归出口
把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。
①号划分为②号,②号划分为③号,③号划分为④号,划分到④号的时候每个区域只有一个不能划分的问题,这就表明存在递归终止条件。
从递归的经典示例开始
一.数组求和
Sum(arr[0...n-1]) = arr[0] + Sum(arr[1...n-1])
后面的 Sum 函数要解决的就是比前一个 Sum 更小的同一问题。
Sum(arr[1...n-1]) = arr[1] + Sum(arr[2...n-1])
以此类推,直到对一个空数组求和,空数组和为 0 ,此时变成了最基本的问题。
Sum(arr[n-1...n-1] ) = arr[n-1] + Sum([])
二.汉诺塔问题
汉诺塔(Hanoi Tower)问题也是一个经典的递归问题,该问题描述如下:
汉诺塔问题:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上。有一个和尚想把这个盘子从A座移到B座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上。
- ① 如果只有 1 个盘子,则不需要利用 B 塔,直接将盘子从 A 移动到 C 。
- ② 如果有 2 个盘子,可以先将盘子 2 上的盘子 1 移动到 B ;将盘子 2 移动到 C ;将盘子 1 移动到 C 。这说明了:可以借助 B 将 2 个盘子从 A 移动到 C ,当然,也可以借助 C 将 2 个盘子从 A 移动到 B 。
- ③ 如果有 3 个盘子,那么根据 2 个盘子的结论,可以借助 C 将盘子 3 上的两个盘子从 A 移动到 B ;将盘子 3 从 A 移动到 C ,A 变成空座;借助 A 座,将 B 上的两个盘子移动到 C 。
- ④ 以此类推,上述的思路可以一直扩展到 n 个盘子的情况,将将较小的 n-1个盘子看做一个整体,也就是我们要求的子问题,以借助 B 塔为例,可以借助空塔 B 将盘子A上面的 n-1 个盘子从 A 移动到 B ;将A 最大的盘子移动到 C , A 变成空塔;借助空塔 A ,将 B 塔上的 n-2 个盘子移动到 A,将 C 最大的盘子移动到 C, B 变成空塔。。。
三.爬台阶问题
问题描述:
一个人爬楼梯,每次只能爬1个或2个台阶,假设有n个台阶,那么这个人有多少种不同的爬楼梯方法?
先从简单的开始,以 4 个台阶为例,可以通过每次爬 1 个台阶爬完楼梯:
可以通过先爬 2 个台阶,剩下的每次爬 1 个台阶爬完楼梯
在这里,可以思考一下:可以根据第一步的走法把所有走法分为两类:
- ① 第一类是第一步走了 1 个台阶
- ② 第二类是第一步走了 2 个台阶
所以 n 个台阶的走法就等于先走 1 阶后,n-1 个台阶的走法 ,然后加上先走 2 阶后,n-2 个台阶的走法。
用公式表示就是:
f(n) = f(n-1)+f(n-2)
有了递推公式,递归代码基本上就完成了一半。那么接下来考虑递归终止条件。
当有一个台阶时,我们不需要再继续递归,就只有一种走法。
所以 f(1)=1
。
通过用 n = 2
,n = 3
这样比较小的数试验一下后发现这个递归终止条件还不足够。
n = 2
时,f(2) = f(1) + f(0)
。如果递归终止条件只有一个f(1) = 1
,那 f(2)
就无法求解,递归无法结束。
所以除了 f(1) = 1
这一个递归终止条件外,还要有 f(0) = 1
,表示走 0 个台阶有一种走法,从思维上以及动图上来看,这显得的有点不符合逻辑。所以为了便于理解,把 f(2) = 2
作为一种终止条件,表示走 2 个台阶,有两种走法,一步走完或者分两步来走。
总结如下:
- ① 假设只有一个台阶,那么只有一种走法,那就是爬 1 个台阶
- ② 假设有两个个台阶,那么有两种走法,一步走完或者分两步来走
通过递归条件:
f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2)
很容易推导出递归代码:
int f(int n) {if (n == 1) return 1;if (n == 2) return 2;return f(n-1) + f(n-2);
}
通过上述三个示例,总结一下如何写递归代码:
- 1.找到如何将大问题分解为小问题的规律
- 2.通过规律写出递推公式
- 3.通过递归公式的临界点推敲出终止条件
- 4.将递推公式和终止条件翻译成代码
什么是动态规划
介绍动态规划之前先介绍一下分治策略(Divide and Conquer)。
分治策略
将原问题分解为若干个规模较小但类似于原问题的子问题(Divide),「递归」的求解这些子问题(Conquer),然后再合并这些子问题的解来建立原问题的解。
因为在求解大问题时,需要递归的求小问题,因此一般用「递归」的方法实现,即自顶向下。
动态规划(Dynamic Programming)
动态规划其实和分治策略是类似的,也是将一个原问题分解为若干个规模较小的子问题,递归的求解这些子问题,然后合并子问题的解得到原问题的解。
区别在于这些子问题会有重叠,一个子问题在求解后,可能会再次求解,于是我们想到将这些子问题的解存储起来,当下次再次求解这个子问题时,直接拿过来就是。
其实就是说,动态规划所解决的问题是分治策略所解决问题的一个子集,只是这个子集更适合用动态规划来解决从而得到更小的运行时间。
即用动态规划能解决的问题分治策略肯定能解决,只是运行时间长了。因此,分治策略一般用来解决子问题相互对立的问题,称为标准分治,而动态规划用来解决子问题重叠的问题。
与「分治策略」「动态规划」概念接近的还有「贪心算法」「回溯算法」,由于篇幅限制,程序员小吴就不在这进行展开,在后续的文章中将分别详细的介绍「贪心算法」、「回溯算法」、「分治算法」,敬请关注:)
将「动态规划」的概念关键点抽离出来描述就是这样的:
- 1.动态规划法试图只解决每个子问题一次
- 2.一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
从递归到动态规划
还是以 爬台阶 为例,如果以递归的方式解决的话,那么这种方法的时间复杂度为O(2^n),具体的计算可以查看笔者之前的文章 《冰与火之歌:时间复杂度与空间复杂度》。
相同颜色代表着 爬台阶问题 在递归计算过程中重复计算的部分。
通过图片可以发现一个现象,我们是 自顶向下 的进行递归运算,比如:f(n)
是f(n-1)
与f(n-2)
相加,f(n-1)
是f(n-2)
与f(n-3)
相加。
思考一下:如果反过来,采取自底向上,用迭代的方式进行推导会怎么样了?
下面通过表格来解释 f(n)
自底向上的求解过程。
台阶数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
走法数 | 1 | 2 |
表格的第一行代表了楼梯台阶的数目,第二行代表了若干台阶对应的走法数。
其中f(1) = 1
和 f(2) = 2
是前面明确的结果。
第一次迭代,如果台阶数为 3 ,那么走法数为 3 ,通过 f(3) = f(2) + f(1)
得来。
台阶数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
走法数 | 1 | 2 | 3 |
第二次迭代,如果台阶数为 4 ,那么走法数为 5 ,通过 f(4) = f(3) + f(2)
得来。
台阶数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
走法数 | 1 | 2 | 3 | 5 |
由此可见,每一次迭代过程中,只需要保留之前的两个状态,就可以推到出新的状态。
show me the code
int f(int n) {if (n == 1) return 1;if (n == 2) return 2;// a 保存倒数第二个子状态数据,b 保存倒数第一个子状态数据, temp 保存当前状态的数据int a = 1, b = 2;int temp = a + b;for (int i = 3; i <= n; i++) {temp = a + b;a = b;b = temp; }return temp;
}
程序从 i = 3
开始迭代,一直到 i = n
结束。每一次迭代,都会计算出多一级台阶的走法数量。迭代过程中只需保留两个临时变量 a 和 b ,分别代表了上一次和上上次迭代的结果。为了便于理解,引入了temp变量。temp代表了当前迭代的结果值。
看一看出,事实上并没有增加太多的代码,只是简单的进行了优化,时间复杂度便就降为O(n),而空间复杂度也变为O(1),这,就是「动态规划」的强大!
详解动态规划
「动态规划」中包含三个重要的概念:
- 【最优子结构】
- 【边界】
- 【状态转移公式】
在「 爬台阶问题 」中
f(10) = f(9) + f(8)
是【最优子结构】
f(1) 与 f(2)
是【边界】
f(n) = f(n-1) + f(n-2)
【状态转移公式】
「 爬台阶问题 」 只是动态规划中相对简单的问题,因为它只有一个变化维度,如果涉及多个维度的话,那么问题就变得复杂多了。
难点就在于找出 「动态规划」中的这三个概念。
比如「 国王和金矿问题 」。
国王和金矿问题
有一个国家发现了 5 座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是 10 人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
找出 「动态规划」中的这三个概念
国王和金矿问题中的【最优子结构】
国王和金矿问题中的【最优子结构】有两个:
- ① 4 金矿 10 工人的最优选择
- ② 4 金矿 (10 - 5) 工人的最优选择
4 金矿的最优选择与 5 金矿的最优选择之间的关系是
MAX[(4 金矿 10 工人的挖金数量),(4 金矿 5 工人的挖金数量 + 第 5 座金矿的挖金数量)]
国王和金矿问题中的【边界】
国王和金矿问题中的【边界】 有两个:
- ① 当只有 1 座金矿时,只能挖这座唯一的金矿,得到的黄金数量为该金矿的数量
- ② 当给定的工人数量不够挖 1 座金矿时,获取的黄金数量为 0
国王和金矿问题中的【状态转移公式】
我们把金矿数量设为 N,工人数设为 W,金矿的黄金量设为数组G[],金矿的用工量设为数组P[],得到【状态转移公式】:
- 边界值:F(n,w) = 0 (n <= 1, w < p[0])
F(n,w) = g[0] (n==1, w >= p[0])
- F(n,w) = F(n-1,w) (n > 1, w < p[n-1])
F(n,w) = max(F(n-1,w), F(n-1,w-p[n-1]) + g[n-1]) (n > 1, w >= p[n-1])
国王和金矿问题中的【实现】
先通过几幅动画来理解 「工人」 与 「金矿」 搭配的方式
1.只挖第一座金矿
在只挖第一座金矿前面两个工人挖矿收益为 零,当有三个工人时,才开始产生收益为 200,而后即使增加再多的工人收益不变,因为只有一座金矿可挖。
2.挖第一座与第二座金矿
在第一座与第二座金矿这种情况中,前面两个工人挖矿收益为 零,因为 W < 3,所以F(N,W) = F(N-1,W) = 0。
当有 三 个工人时,将其安排挖第 一 个金矿,开始产生收益为 200。
当有 四 个工人时,挖矿位置变化,将其安排挖第 二 个金矿,开始产生收益为 300。
当有 五、六 个工人时,由于多于 四 个工人的人数不足以去开挖第 一 座矿,因此收益还是为 300。
当有 七 个工人时,可以同时开采第 一 个和第 二 个金矿,开始产生收益为 500。
3.挖前三座金矿
这是「国王和金矿」 问题中最重要的一个动画之一,可以多看几遍
4.挖前四座金矿
这是「国王和金矿」 问题中最重要的一个动画之一,可以多看几遍
《LeetCode刷题C/C++版答案》pdf出炉,白瞟党乐坏了
国王和金矿问题中的【规律】
仔细观察上面的几组动画可以发现:
- 对比「挖第一座与第二座金矿」和「挖前三座金矿」,在「挖前三座金矿」中,3 金矿 7 工人的挖矿收益,来自于 2 金矿 7 工人和 2 金矿 4 工人的结果,Max(500,300 + 350) = 650;
- 对比「挖前三座金矿」和「挖前四座金矿」,在「挖前四座金矿」中,4 金矿 10 工人的挖矿收益,来自于 3 金矿 10 工人和 3 金矿 5 工人的结果,Max(850,400 + 300) = 850;
国王和金矿问题中的【动态规划代码】
代码来源:https://www.cnblogs.com/SDJL/archive/2008/08/22/1274312.html//maxGold[i][j] 保存了i个人挖前j个金矿能够得到的最大金子数,等于 -1 时表示未知
int maxGold[max_people][max_n];int GetMaxGold(int people, int mineNum){int retMaxGold; //声明返回的最大金矿数量//如果这个问题曾经计算过if(maxGold[people][mineNum] != -1){retMaxGold = maxGold[people][mineNum]; //获得保存起来的值}else if(mineNum == 0) { //如果仅有一个金矿时 [ 对应动态规划中的"边界"]if(people >= peopleNeed[mineNum]) //当给出的人数足够开采这座金矿retMaxGold = gold[mineNum]; //得到的最大值就是这座金矿的金子数else //否则这唯一的一座金矿也不能开采retMaxGold = 0; //得到的最大值为 0 个金子}else if(people >= peopleNeed[mineNum]) // 如果人够开采这座金矿[对应动态规划中的"最优子结构"]{//考虑开采与不开采两种情况,取最大值retMaxGold = max(GetMaxGold(people - peopleNeed[mineNum],mineNum - 1) + gold[mineNum],GetMaxGold(people,mineNum - 1));}else//否则给出的人不够开采这座金矿 [ 对应动态规划中的"最优子结构"]{retMaxGold = GetMaxGold(people,mineNum - 1); //仅考虑不开采的情况maxGold[people][mineNum] = retMaxGold;}return retMaxGold;
}
学妹半夜问我动态规划是咋回事,我啪的一下讲清楚了!相关推荐
- 学妹跑过来问我为啥使用Xshell连接虚拟机时连接需要等那么久【手把手讲解】
解决[使用shell连接虚拟机时连接等待时长过长]的问题 打开sshd服务的配置文件/etc/ssh/sshd_config 把UseDNS yes,改为UseDNS no 重启ssh服务 打开ssh ...
- 深夜里学妹竟然问我会不会C?我直接把这篇文章甩她脸上(C Primer Plus 第六版基础整合)
C Primer Plus 第六版 前言 第一章 初识C语言 一.C语言的起源 二.C语言的应用 三.C语言的特点 四.编译的过程 五.编码机制 1.简述 2.完成机制 六.在UNIX系统上使用C 七 ...
- 刚学会深拷贝一个对象,学妹却问我怎么深拷贝一个图
前言 在前面,我写过一篇Java的深浅拷贝,那是基于对象的拷贝,但放眼数据结构与算法中,你有考虑过怎么拷贝一个图吗?(无向图) 在此之前,你需要对一些概念搞清楚:什么是深拷贝.浅拷贝? 浅拷贝:如果拷 ...
- 学妹问H哥:你是如何平衡工作和生活的?
作者 l Hollis 来源 l Hollis(ID:hollischuang) 这几天被B站的<后浪>刷屏了,不管别人怎么看,我个人觉得还挺不错的.很多人说<后浪>传达出一种 ...
- 坯子库曲面推拉教程_为了学妹,掉几根头发算什么!学长熬夜整理的SU插件合集大放送!...
学妹 学妹 学长,我刚学SU,一般都要装什么插件呀? 小智本智 SU插件太多了,装些常用的就可以 学妹 学长有安装包么,能不能发我? 小智本智 我这有套SU插件合集,基本要用的都有,还有教程,你跟着学 ...
- 学妹问我Java枚举类与注解,我直接用这个搞定她!
很多人问我学妹长什么样,不多说 上图吧! 学妹问我Java枚举类与注解,我直接一篇文章搞定! 一.枚举类 ① 自定义枚举类 ② enum关键字定义枚举类 ③ enum 枚举类的方法 ④ enum 枚举 ...
- 【熬夜猛肝万字博文】学妹问我怎么入门 Javascript,百般盘问下我终于决定贡献出自己的 JavaScript入门笔记(三)
你好,我是阿ken?? 版权声明:本文为CSDN博主「」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明. 另外,博文中某些图片或内容可能出自网络,如有侵权或问题,请及 ...
- python记忆口诀-学妹问我: 如何提高编程能力
聊天截图 聊天截图 前言 开局两张图,剩下全靠吹了. 上面这两张图便是写这篇文章的原由. 对话框的另一边,是一位大二计算机科班在读的小姐姐,看似平静的文字背后透露着迷茫与困惑,还对未来的焦虑. 透过屏 ...
- python记忆口诀-学妹问我:如何提高编程能力
聊天截图 聊天截图前言 开局两张图,剩下全靠吹了. 上面这两张图便是写这篇文章的原由. 对话框的另一边,是一位大二计算机科班在读的小姐姐,看似平静的文字背后透露着迷茫与困惑,还对未来的焦虑. 透过屏幕 ...
最新文章
- python3 队列 queue
- C++重载下标操作符[](二)
- Cocos2dx学习笔记(1) Ref类型数据 垃圾回收机制
- OAuth 2.0介绍
- 【MySQL笔记】: unable to connect to remote host. catalog download has failed.
- windows编译python扩展Unable to find vcvarsall
- Linux与Windows编译器的区别
- python实现局域网内传输文件
- 通信原理教程chapter1
- 京东Java面试题、笔试题(含答案)
- 北理工慕课 嵩天 Python零基础入门 笔记整理
- 0的ascii码值(0的ascii码值)
- matlab仿真高斯脉冲,高斯脉冲comsol仿真
- linux安装vim失败(Unable to locate package vim)
- c语言中特殊符号怎么定义,C语言特殊符号意义
- TFN TT70网络综合分析仪性能如何
- 强化学习笔记:多臂老虎机问题(2)--Python仿真
- python游戏制作rpg_用 Python 语言来写游戏
- CentOS vs REHL、鸿蒙vs Fuchsia,操作系统岁末大盘点
- Linux学习-HaProxy代理后端Nginx