@@ -50,7 +50,7 @@ def f(n):
5050
5151#### 不仅仅是普通的递归函数
5252
53- 本文中所提到的记忆化递归中的递归函数实际上** 指的是特殊的递归 ** ,即在普通的递归上满足以下几个条件 :
53+ 本文中所提到的记忆化递归中的递归函数实际上** 指的是特殊的递归函数 ** ,即在普通的递归函数上满足以下几个条件 :
5454
55551 . 递归函数不依赖外部变量
56562 . 递归函数不改变外部变量
@@ -73,15 +73,18 @@ def f(x):
7373 return x + f(x - 1 )
7474```
7575
76- - x 就是自变量
77- - f(x) 就是函数
76+ - x 就是自变量,x 的所有可能的返回值构成的集合就是定义域。
77+ - f(x) 就是函数。
78+ - f(x) 的所有可能的返回值构成的集合就是值域。
7879
79- 自变量也可以有多个,对应递归函数的参数可以有多个。
80+ 自变量也可以有多个,对应递归函数的参数可以有多个,比如 f(x1, x2, x3) 。
8081
8182** 通过函数来描述问题,并通过函数的调用关系来描述问题间的关系就是记忆化递归的核心内容。**
8283
8384每一个动态规划问题,实际上都可以抽象为一个数学上的函数。这个函数的自变量集合就是题目的所有取值,值域就是题目要求的答案的所有可能。我们的目标其实就是填充这个函数的内容,使得给定自变量 x,能够唯一映射到一个值 y。(当然自变量可能有多个,对应递归函数参数可能有多个)
8485
86+ ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gplrxy60mpj30pt0daacn.jpg )
87+
8588递归并不是算法,它是和迭代对应的一种编程方法。只不过,我们通常借助递归去分解问题而已。比如我们定义一个递归函数 f(n),用 f(n) 来描述问题。就和使用普通动态规划 f[ n] 描述问题是一样的,这里的 f 是 dp 数组。
8689
8790### 什么是记忆化?
@@ -92,7 +95,7 @@ def f(x):
9295
9396思路:
9497
95- 由于上 ** 第 n 级台阶一定是从 n - 1 或者 n - 2 来的 ** ,因此 上第 n 级台阶的数目就是 ` 上 n - 1 级台阶的数目加上 n - 1 级台阶的数目` 。
98+ 由于 ** 第 n 级台阶一定是从 n - 1 级台阶或者 n - 2 级台阶来的 ** ,因此到第 n 级台阶的数目就是 ` 到第 n - 1 级台阶的数目加上到第 n - 1 级台阶的数目` 。
9699
97100递归代码:
98101
@@ -104,15 +107,15 @@ function climbStairs(n) {
104107}
105108```
106109
107- 我们用一个递归树来直观感受以下:
110+ 我们用一个递归树来直观感受以下(每一个圆圈表示一个子问题) :
108111
109112![ dynamic-programming-2] ( https://tva1.sinaimg.cn/large/007S8ZIlly1ghluhw6pf2j30mz0b2dgk.jpg )
110113
111- 红色表示重复的计算。即 Fib(N-2) 和 Fib(N-3) 都被计算了两次,实际上计算一次就够了。比如第一次计算出了 Fib(N-2),下次再次计算 Fib(N-2),则可以直接将上次计算的结果返回。之所以能够这样的做的原因还是上面我讲的 ** 纯函数 ** ,即相同的参数经过同一函数处理必然得到同样的值 。
114+ 红色表示重复的计算。即 Fib(N-2) 和 Fib(N-3) 都被计算了两次,实际上计算一次就够了。比如第一次计算出了 Fib(N-2) 的值,那么下次再次需要计算 Fib(N-2)的时候,可以直接将上次计算的结果返回。之所以可以这么做的原因正是前文提到的 ** 我们的递归函数是数学中的函数,也就是说参数一定,那么返回值也一定不会变 ** ,因此下次如果碰到相同的参数,我们就可以 ** 将上次计算过的值直接返回,而不必重新计算 ** 。这样节省的时间就等价于重叠子问题的个数 。
112115
113- 不难看出这里面有很多重复计算 ,我们可以使用一个 hashtable 去缓存中间计算结果,从而省去不必要的计算。之所以可以这么做的原因正是前文提到的 ** 我们的递归函数是数学中的函数,也就是说参数一定,那么返回值也一定不会变 ** ,因此下次如果碰到相同的参数,我们就可以 ** 将上次计算过的值直接返回,而不必重新计算 ** 。这样节省的时间就等价于重叠子问题的个数 。
116+ 代码上 ,我们可以使用一个 hashtable 去缓存中间计算结果,从而省去不必要的计算。
114117
115- 代码 :
118+ 我们使用记忆化来改造上面的代码 :
116119
117120``` py
118121memo = {}
@@ -128,11 +131,7 @@ climbStairs(10)
128131
129132这里我使用了一个名为 ** memo 的哈希表来存储递归函数的返回值,其中 key 为参数,value 为递归函数的返回值。** 大家可以通过删除和添加代码中的 memo 来感受一下** 记忆化** 的作用。
130133
131- 递归中** 如果** 存在重复计算(我们称重叠子问题,下文会讲到),那就是使用动态规划解题的强有力信号之一。
132-
133- 如果没有重叠子问题,直接暴力求解就好了,无需使用动态规划。可以看出动态规划的核心就是使用记忆化的手段消除重复子问题的计算,如果这种重复子问题的规模是指数或者更高规模,那么动态规划带来的收益会非常大。
134-
135- 为了消除这种重复计算,一种简单的方式就是记忆化递归。即一边递归一边使用“记录表”(比如哈希表或者数组)记录我们已经计算过的情况,当下次再次碰到的时候,如果之前已经计算了,那么直接返回即可,这样就避免了重复计算。而** 动态规划中 DP 数组其实和这里“记录表”的作用是一样的** 。
134+ (图 xxx)
136135
137136### 小结
138137
@@ -148,6 +147,12 @@ climbStairs(10)
148147
149148- 杨辉三角
150149
150+ 递归中** 如果** 存在重复计算(我们称重叠子问题,下文会讲到),那就是使用动态规划解题的强有力信号之一。
151+
152+ 如果没有重叠子问题,直接暴力求解就好了,无需使用动态规划。可以看出动态规划的核心就是使用记忆化的手段消除重复子问题的计算,如果这种重复子问题的规模是指数或者更高规模,那么动态规划带来的收益会非常大。
153+
154+ 为了消除这种重复计算,一种简单的方式就是记忆化递归。即一边递归一边使用“记录表”(比如哈希表或者数组)记录我们已经计算过的情况,当下次再次碰到的时候,如果之前已经计算了,那么直接返回即可,这样就避免了重复计算。而** 动态规划中 DP 数组其实和这里“记录表”的作用是一样的** 。
155+
151156如果你刚开始接触递归, 建议大家先去练习一下递归再往后看。一个简单练习递归的方式是将你写的迭代全部改成递归形式。比如你写了一个程序,功能是“将一个字符串逆序输出”,那么使用迭代将其写出来会非常容易,那么你是否可以使用递归写出来呢?通过这样的练习,可以让你逐步适应使用递归来写程序。
152157
153158当你已经适应了递归的时候,那就让我们继续学习动态规划吧!
@@ -158,11 +163,11 @@ climbStairs(10)
158163
159164### 动态规划的基本概念
160165
161- 我们先来学习动态规划最重要的四个概念:最优子结构,无后效性 。
166+ 我们先来学习动态规划最重要的两个概念:最优子结构和无后效性 。
162167
163168其中:
164169
165- - 无后效性决定了什么时候可使用动态规划来解决 。
170+ - 无后效性决定了是否可使用动态规划来解决 。
166171- 最优子结构决定了具体如何解决。
167172
168173#### 最优子结构
@@ -177,12 +182,12 @@ climbStairs(10)
177182
178183再比如 01 背包问题:定义 f(weights, values, capicity)。如果我们想要求 f([ 1,2,3] , [ 2,2,4] , 10) 的最优解。我们可以将其划分为如下子问题:
179184
180- - f([ 1,2] , [ 2,2] , 10)
181- - 和 f([ 1,2,3] , [ 2,2,4] , 9)
185+ - ` 将第三件物品装进背包 ` ,也就是 f([ 1,2] , [ 2,2] , 10)
186+ - 和` 不将第三件物品装进背包 ` ,也就是 f([ 1,2,3] , [ 2,2,4] , 9)
182187
183188> 显然这两个问题还是复杂,我们需要进一步拆解。不过,这里不是讲如何拆解的。
184189
185- 原问题 f([ 1,2,3] , [ 2,2,4] , 10) 等于以上两个子问题的最大值。而这两个子问题 ** 一定也是最优的 ** ,不然就无法得到 f( [ 1,2,3 ] , [ 2,2,4 ] , 10) 的最优解 。
190+ 原问题 f([ 1,2,3] , [ 2,2,4] , 10) 等于以上两个子问题的最大值。只有两个子问题都是 ** 最优的 ** 时候整体才是最优的,这是因为子问题之间不会相互影响 。
186191
187192#### 无后效性
188193
@@ -191,7 +196,7 @@ climbStairs(10)
191196继续以上面两个例子来说。
192197
193198- 数学考得高不能影响英语(现实其实可能影响,比如时间一定,投入英语多,其他科目就少了)。
194- - 背包问题中 f([ 1,2,3] , [ 2,2,4] , 10) 选择是否拿第三件物品,不应该影响是否拿前面的物品。比如题目规定了拿了第三件物品之后,第二件物品的价值就变成了 x 。这种情况就不满足无后向性。
199+ - 背包问题中 f([ 1,2,3] , [ 2,2,4] , 10) 选择是否拿第三件物品,不应该影响是否拿前面的物品。比如题目规定了拿了第三件物品之后,第二件物品的价值就会变低或变高) 。这种情况就不满足无后向性。
195200
196201#### 动态规划三要素
197202
0 commit comments