@@ -50,7 +50,7 @@ def f(n):
50
50
51
51
#### 不仅仅是普通的递归函数
52
52
53
- 本文中所提到的记忆化递归中的递归函数实际上** 指的是特殊的递归 ** ,即在普通的递归上满足以下几个条件 :
53
+ 本文中所提到的记忆化递归中的递归函数实际上** 指的是特殊的递归函数 ** ,即在普通的递归函数上满足以下几个条件 :
54
54
55
55
1 . 递归函数不依赖外部变量
56
56
2 . 递归函数不改变外部变量
@@ -73,15 +73,18 @@ def f(x):
73
73
return x + f(x - 1 )
74
74
```
75
75
76
- - x 就是自变量
77
- - f(x) 就是函数
76
+ - x 就是自变量,x 的所有可能的返回值构成的集合就是定义域。
77
+ - f(x) 就是函数。
78
+ - f(x) 的所有可能的返回值构成的集合就是值域。
78
79
79
- 自变量也可以有多个,对应递归函数的参数可以有多个。
80
+ 自变量也可以有多个,对应递归函数的参数可以有多个,比如 f(x1, x2, x3) 。
80
81
81
82
** 通过函数来描述问题,并通过函数的调用关系来描述问题间的关系就是记忆化递归的核心内容。**
82
83
83
84
每一个动态规划问题,实际上都可以抽象为一个数学上的函数。这个函数的自变量集合就是题目的所有取值,值域就是题目要求的答案的所有可能。我们的目标其实就是填充这个函数的内容,使得给定自变量 x,能够唯一映射到一个值 y。(当然自变量可能有多个,对应递归函数参数可能有多个)
84
85
86
+ ![ ] ( https://tva1.sinaimg.cn/large/008eGmZEly1gplrxy60mpj30pt0daacn.jpg )
87
+
85
88
递归并不是算法,它是和迭代对应的一种编程方法。只不过,我们通常借助递归去分解问题而已。比如我们定义一个递归函数 f(n),用 f(n) 来描述问题。就和使用普通动态规划 f[ n] 描述问题是一样的,这里的 f 是 dp 数组。
86
89
87
90
### 什么是记忆化?
@@ -92,7 +95,7 @@ def f(x):
92
95
93
96
思路:
94
97
95
- 由于上 ** 第 n 级台阶一定是从 n - 1 或者 n - 2 来的 ** ,因此 上第 n 级台阶的数目就是 ` 上 n - 1 级台阶的数目加上 n - 1 级台阶的数目` 。
98
+ 由于 ** 第 n 级台阶一定是从 n - 1 级台阶或者 n - 2 级台阶来的 ** ,因此到第 n 级台阶的数目就是 ` 到第 n - 1 级台阶的数目加上到第 n - 1 级台阶的数目` 。
96
99
97
100
递归代码:
98
101
@@ -104,15 +107,15 @@ function climbStairs(n) {
104
107
}
105
108
```
106
109
107
- 我们用一个递归树来直观感受以下:
110
+ 我们用一个递归树来直观感受以下(每一个圆圈表示一个子问题) :
108
111
109
112
![ dynamic-programming-2] ( https://tva1.sinaimg.cn/large/007S8ZIlly1ghluhw6pf2j30mz0b2dgk.jpg )
110
113
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)的时候,可以直接将上次计算的结果返回。之所以可以这么做的原因正是前文提到的 ** 我们的递归函数是数学中的函数,也就是说参数一定,那么返回值也一定不会变 ** ,因此下次如果碰到相同的参数,我们就可以 ** 将上次计算过的值直接返回,而不必重新计算 ** 。这样节省的时间就等价于重叠子问题的个数 。
112
115
113
- 不难看出这里面有很多重复计算 ,我们可以使用一个 hashtable 去缓存中间计算结果,从而省去不必要的计算。之所以可以这么做的原因正是前文提到的 ** 我们的递归函数是数学中的函数,也就是说参数一定,那么返回值也一定不会变 ** ,因此下次如果碰到相同的参数,我们就可以 ** 将上次计算过的值直接返回,而不必重新计算 ** 。这样节省的时间就等价于重叠子问题的个数 。
116
+ 代码上 ,我们可以使用一个 hashtable 去缓存中间计算结果,从而省去不必要的计算。
114
117
115
- 代码 :
118
+ 我们使用记忆化来改造上面的代码 :
116
119
117
120
``` py
118
121
memo = {}
@@ -128,11 +131,7 @@ climbStairs(10)
128
131
129
132
这里我使用了一个名为 ** memo 的哈希表来存储递归函数的返回值,其中 key 为参数,value 为递归函数的返回值。** 大家可以通过删除和添加代码中的 memo 来感受一下** 记忆化** 的作用。
130
133
131
- 递归中** 如果** 存在重复计算(我们称重叠子问题,下文会讲到),那就是使用动态规划解题的强有力信号之一。
132
-
133
- 如果没有重叠子问题,直接暴力求解就好了,无需使用动态规划。可以看出动态规划的核心就是使用记忆化的手段消除重复子问题的计算,如果这种重复子问题的规模是指数或者更高规模,那么动态规划带来的收益会非常大。
134
-
135
- 为了消除这种重复计算,一种简单的方式就是记忆化递归。即一边递归一边使用“记录表”(比如哈希表或者数组)记录我们已经计算过的情况,当下次再次碰到的时候,如果之前已经计算了,那么直接返回即可,这样就避免了重复计算。而** 动态规划中 DP 数组其实和这里“记录表”的作用是一样的** 。
134
+ (图 xxx)
136
135
137
136
### 小结
138
137
@@ -148,6 +147,12 @@ climbStairs(10)
148
147
149
148
- 杨辉三角
150
149
150
+ 递归中** 如果** 存在重复计算(我们称重叠子问题,下文会讲到),那就是使用动态规划解题的强有力信号之一。
151
+
152
+ 如果没有重叠子问题,直接暴力求解就好了,无需使用动态规划。可以看出动态规划的核心就是使用记忆化的手段消除重复子问题的计算,如果这种重复子问题的规模是指数或者更高规模,那么动态规划带来的收益会非常大。
153
+
154
+ 为了消除这种重复计算,一种简单的方式就是记忆化递归。即一边递归一边使用“记录表”(比如哈希表或者数组)记录我们已经计算过的情况,当下次再次碰到的时候,如果之前已经计算了,那么直接返回即可,这样就避免了重复计算。而** 动态规划中 DP 数组其实和这里“记录表”的作用是一样的** 。
155
+
151
156
如果你刚开始接触递归, 建议大家先去练习一下递归再往后看。一个简单练习递归的方式是将你写的迭代全部改成递归形式。比如你写了一个程序,功能是“将一个字符串逆序输出”,那么使用迭代将其写出来会非常容易,那么你是否可以使用递归写出来呢?通过这样的练习,可以让你逐步适应使用递归来写程序。
152
157
153
158
当你已经适应了递归的时候,那就让我们继续学习动态规划吧!
@@ -158,11 +163,11 @@ climbStairs(10)
158
163
159
164
### 动态规划的基本概念
160
165
161
- 我们先来学习动态规划最重要的四个概念:最优子结构,无后效性 。
166
+ 我们先来学习动态规划最重要的两个概念:最优子结构和无后效性 。
162
167
163
168
其中:
164
169
165
- - 无后效性决定了什么时候可使用动态规划来解决 。
170
+ - 无后效性决定了是否可使用动态规划来解决 。
166
171
- 最优子结构决定了具体如何解决。
167
172
168
173
#### 最优子结构
@@ -177,12 +182,12 @@ climbStairs(10)
177
182
178
183
再比如 01 背包问题:定义 f(weights, values, capicity)。如果我们想要求 f([ 1,2,3] , [ 2,2,4] , 10) 的最优解。我们可以将其划分为如下子问题:
179
184
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)
182
187
183
188
> 显然这两个问题还是复杂,我们需要进一步拆解。不过,这里不是讲如何拆解的。
184
189
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) 等于以上两个子问题的最大值。只有两个子问题都是 ** 最优的 ** 时候整体才是最优的,这是因为子问题之间不会相互影响 。
186
191
187
192
#### 无后效性
188
193
@@ -191,7 +196,7 @@ climbStairs(10)
191
196
继续以上面两个例子来说。
192
197
193
198
- 数学考得高不能影响英语(现实其实可能影响,比如时间一定,投入英语多,其他科目就少了)。
194
- - 背包问题中 f([ 1,2,3] , [ 2,2,4] , 10) 选择是否拿第三件物品,不应该影响是否拿前面的物品。比如题目规定了拿了第三件物品之后,第二件物品的价值就变成了 x 。这种情况就不满足无后向性。
199
+ - 背包问题中 f([ 1,2,3] , [ 2,2,4] , 10) 选择是否拿第三件物品,不应该影响是否拿前面的物品。比如题目规定了拿了第三件物品之后,第二件物品的价值就会变低或变高) 。这种情况就不满足无后向性。
195
200
196
201
#### 动态规划三要素
197
202
0 commit comments