Skip to content

Commit 6e26c56

Browse files
author
lucifer
committed
feat: $220
1 parent 06a248d commit 6e26c56

File tree

2 files changed

+30
-23
lines changed

2 files changed

+30
-23
lines changed

problems/220.contains-duplicate-iii.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,17 @@ https://leetcode-cn.com/problems/contains-duplicate-iii/
4141

4242
这里我们介绍一种分桶的思想,其基本思想和桶排序是类似的。
4343

44-
具体来说,我们可使用 t 个桶。将所有数除以 (t+1) 的结果**作为编号都存到一个哈希表中**不难知道哈希表的大小为 t。如果两个数字的编号相同,那么意味着其绝对值差小于等于 t
44+
具体来说,我们可使用 (t + 1) 个桶。将所有数除以 (t+1) 的结果**作为编号存到一个哈希表中**不难知道哈希表的编号范围为 [0, t],因此哈希表最大容量为 (t+1)
4545

46-
那么如果两个数字的编号不同,是否意味着其绝对值差大于 t 呢?也不一定,相邻编号也可能是绝对值差小于等于 t 。因此我们只需要检查以下三种情况即可。
46+
经过这样的处理,如果两个数字的编号相同,那么意味着其绝对值差小于等于 t。
47+
48+
那么如果两个数字的编号不同,是否意味着其绝对值差大于 t 呢?也不一定,**相邻编号**也可能是绝对值差小于等于 t 。因此我们只需要检查以下三种情况即可。
4749

4850
1. 当前编号
4951
2. 左边相邻的编号
5052
3. 右边相邻的编号
5153

52-
另外由于题目限定是索引差小于等于 k,因此需要清除哈希表中过期的信息
54+
另外由于题目限定是索引差小于等于 k,因此我们可以固定一个窗口大小为 k 的滑动窗口,每次都仅处理窗口内的元素,这样可以保证桶内的数任意两个数都满足**索引之差的绝对值小于等于 k**。 因此我们需要清除哈希表中过期(不在窗口内)的信息
5355

5456
## 关键点
5557

thinkings/dynamic-programming.md

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def f(n):
5050
5151
#### 不仅仅是普通的递归函数
5252

53-
本文中所提到的记忆化递归中的递归函数实际上**指的是特殊的递归**即在普通的递归上满足以下几个条件
53+
本文中所提到的记忆化递归中的递归函数实际上**指的是特殊的递归函数**即在普通的递归函数上满足以下几个条件
5454

5555
1. 递归函数不依赖外部变量
5656
2. 递归函数不改变外部变量
@@ -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
118121
memo = {}
@@ -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

Comments
 (0)