88
99动态规划方法与分治算法类似,却又不同于分治算法。
1010
11- 动态规划的核心思想是 :
11+ 「动态规划的核心思想」是 :
1212
13131 .  把「原问题」分解为「若干个重叠的子问题」,每个子问题的求解过程都构成一个 ** 「阶段」** 。在完成一个阶段的计算之后,动态规划方法才会执行下一个阶段的计算。
14142 .  在求解子问题的过程中,按照自底向上的顺序求解出「子问题的解」,把结果存储在表格中,当需要再次求解此子问题时,直接从表格中查询该子问题的解,从而避免了大量的重复计算。
1515
16- 动态规划方法与分治算法的不同点在于 :适用于动态规划求解的问题,在分解之后得到的子问题往往是相互联系的,会出现若干个重叠子问题。使用动态规划方法会将这些重叠子问题的解保存到表格里,供随后的计算查询使用,从而避免大量的重复计算。
16+ 「动态规划方法与分治算法的不同点」在于 :适用于动态规划求解的问题,在分解之后得到的子问题往往是相互联系的,会出现若干个重叠子问题。使用动态规划方法会将这些重叠子问题的解保存到表格里,供随后的计算查询使用,从而避免大量的重复计算。
1717
1818### 1.2 动态规划的特征  
1919
3131
3232#### 1.2.2 重叠子问题性质  
3333
34- 「重叠子问题性质」:指的是在求解子问题的过程中,有大量的子问题是重复的,一个子问题会在下一阶段的决策中可能会被多次用到。如果有大量重复的子问题,那么只需要对求解一次,然后用表格将结果存储下来,以后使用时可以直接查询,不需要再次求解。从而避免重复求解相同的子问题,提升效率。
34+ 「重叠子问题性质」:指的是在求解子问题的过程中,有大量的子问题是重复的,一个子问题会在下一阶段的决策中可能会被多次用到。如果有大量重复的子问题,那么只需要对求解一次,然后用表格将结果存储下来,以后使用时可以直接查询,不需要再次求解。
35+ 
36+ 举个例子,比如斐波那契数列的定义是:` f(1) = 1, f(2) = 2, f(n) = f(n - 1) + f(n - 2) ` 。对应的递推过程如下图所示,其中 ` f(1) ` 、` f(2) ` 、` f(3) ` 、` f(4) `  都进行了多次重复计算。而如果我们在第一次计算 ` f(1) ` 、` f(2) ` 、` f(3) ` 、` f(4) `  时就将其结果存入表格,则再次使用时可以直接查询,从而避免重复求解相同的子问题,提升效率。
37+ 
38+ ![ ] ( https://qcdn.itcharge.cn/images/202207202039896.png ) 
3539
3640#### 1.2.3 无后效性  
3741
38- 「无后效性」:指的是子问题的解一旦确定, 就不再改变,不受在这之后、包含它的更大的问题的求解决策影响 。
42+ 「无后效性」:指的是子问题的解(状态值)只与之前阶段有关,而与后面阶段无关。当前阶段的若干状态值一旦确定, 就不再改变,不会再受到后续阶段决策的影响。换句话说, ** 一旦某一个子问题的求解结果确定以后,就不会再被修改 ** 。
3943
40- 将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。 
44+ 其实我们也可以把动态规划方法的求解过程,看做是有向无环图的最长(最短)路的求解过程。每个状态对应有向无环图上的一个节点,决策对应图中的一条有向边。
45+ 
46+ 如果一个问题具有「后效性」,则可能需要将其转化或者逆向求解来消除后效性,然后才可以使用动态规划方法。
4147
4248## 2. 动态规划的基本思路  
4349
4753
4854![ ] ( https://qcdn.itcharge.cn/images/20220720180135.png ) 
4955
50- 这种前后关联、具有链状结构的多阶段进行决策的问题也叫做「多阶段决策问题」。通常我们使用动态规划方法来解决多阶段决策问题。的基本思路如下: 
56+ 这种前后关联、具有链状结构的多阶段进行决策的问题也叫做「多阶段决策问题」。
5157
52- 对于一个能用动态规划解决的多阶段决策问题,一般采用如下思路解决 :
58+ 通常我们使用动态规划方法来解决多阶段决策问题,其基本思路如下 :
5359
54601 .  ** 划分阶段** :将原问题按顺序(时间顺序、空间顺序或其他顺序)分解为若干个相互联系的「阶段」。划分后的阶段⼀定是有序或可排序的,否则问题⽆法求解。
5561   -  这里的「阶段」指的是⼦问题的求解过程。每个⼦问题的求解过程都构成⼀个「阶段」,在完成前⼀阶段的求解后才会进⾏后⼀阶段的求解。
59654 .  ** 初始条件和边界条件** :根据问题描述、状态定义和状态转移方程,确定初始条件和边界条件。
60665 .  ** 最终结果** :确定问题的求解目标,然后按照一定顺序求解每一个阶段的问题。最后根据状态转移方程的递推结果,确定最终结果。
6167
62- 动态规划相关的问题灵活多变,经常在各类算法竞赛和面试中出现。这类问题需要多练习、多总结,积累丰富的经验和发挥创造⼒。
63- 
6468## 3. 动态规划的应用  
6569
66- ### 3.1 爬楼梯  
70+ 动态规划相关的问题往往灵活多变,思维难度大,没有特别明显的套路,并且经常会在各类算法竞赛和面试中出现。
71+ 
72+ 动态规划问题的关键点在于「如何状态设计」和「推导状态转移条件」,还有各种各样的「优化方法」。这类问题一定要多练习、多总结,只有接触的题型多了,才能熟练掌握动态规划思想。
73+ 
74+ 下面来介绍几道关于动态规划的基础题目。
75+ 
76+ ### 3.1 斐波那契数  
6777
6878#### 3.1.1 题目链接  
6979
70- -  [ 70. 爬楼梯  - 力扣] ( https://leetcode.cn/problems/climbing-stairs / ) 
80+ -  [ 509. 斐波那契数  - 力扣] ( https://leetcode.cn/problems/fibonacci-number / ) 
7181
7282#### 3.1.2 题目大意  
7383
84+ ** 描述** :给定一个整数 ` n ` 。
85+ 
86+ ** 要求** :计算第 ` n `  个斐波那契数。
87+ 
88+ ** 说明** :
89+ 
90+ -  斐波那契数列的定义如下:
91+   -  ` f(0) = 0, f(1) = 1 ` 。
92+   -  ` f(n) = f(n - 1) + f(n - 2) ` ,其中 ` n > 1 ` 。
93+ 
94+ 
95+ ** 示例** :
96+ 
97+ ``` Python 
98+ 输入    n =  2 
99+ 输出    1 
100+ 解释    F(2 ) =  F(1 ) +  F(0 ) =  1  +  0  =  1 
101+ ``` 
102+ 
103+ #### 3.1.3 解题思路  
104+ 
105+ ###### 1. 划分阶段  
106+ 
107+ 我们可以按照整数顺序进行阶段划分,将其划分为整数 ` 0 `  ~ ` n ` 。
108+ 
109+ ###### 2. 定义状态  
110+ 
111+ 定义状态 ` dp[i] `  为:第 ` i `  个斐波那契数。
112+ 
113+ ###### 3. 状态转移方程  
114+ 
115+ 根据题目中所给的斐波那契数列的定义 ` f(n) = f(n - 1) + f(n - 2) ` ,则直接得出状态转移方程为 ` dp[i] = dp[i - 1] + dp[i - 2] ` 。
116+ 
117+ ###### 4. 初始条件  
118+ 
119+ 根据题目中所给的初始条件 ` f(0) = 0, f(1) = 1 `  确定动态规划的初始条件,即 ` dp[0] = 0, dp[1] = 1 ` 。
120+ 
121+ ###### 5. 最终结果  
122+ 
123+ 根据状态定义,最终结果为 ` dp[n] ` ,即第 ` n `  个斐波那契数为 ` dp[n] ` 。
124+ 
125+ #### 3.1.4 代码  
126+ 
127+ ``` Python 
128+ class  Solution :
129+     def  fib (self n : int ) -> int :
130+         if  n <=  1 :
131+             return  n
132+ 
133+         dp =  [0  for  _ in  range (n +  1 )]
134+         dp[0 ] =  0 
135+         dp[1 ] =  1 
136+         for  i in  range (2 , n +  1 ):
137+             dp[i] =  dp[i -  2 ] +  dp[i -  1 ]
138+ 
139+         return  dp[n]
140+ ``` 
141+ 
142+ #### 3.1.5 复杂度分析  
143+ 
144+ -  ** 时间复杂度** :$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。
145+ -  ** 空间复杂度** :$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 ` dp[i] `  的状态只依赖于 ` dp[i - 1] `  和 ` dp[i - 2] ` ,所以可以使用 ` 3 `  个变量来分别表示 ` dp[i] ` 、` dp[i - 1] ` 、` dp[i - 2] ` ,从而将空间复杂度优化到 $O(1)$。
146+ 
147+ ### 3.2 爬楼梯  
148+ 
149+ #### 3.2.1 题目链接  
150+ 
151+ -  [ 70. 爬楼梯 - 力扣] ( https://leetcode.cn/problems/climbing-stairs/ ) 
152+ 
153+ #### 3.2.2 题目大意  
154+ 
74155** 描述** :假设你正在爬楼梯。需要 ` n `  阶你才能到达楼顶。每次你可以爬 ` 1 `  或 ` 2 `  个台阶。现在给定一个整数 ` n ` 。
75156
76157** 要求** :计算出有多少种不同的方法可以爬到楼顶。
901713 . 2 阶 +  1  阶
91172``` 
92173
93- #### 3.1 .3 解题思路  
174+ #### 3.2 .3 解题思路  
94175
95176###### 1. 划分阶段  
96177
97- 按照台阶的层数进行划分为  ` 1 `  ~ ` n ` 。
178+ 我们按照台阶的阶层划分阶段,将其划分为  ` 0 `  ~ ` n `  阶 。
98179
99180###### 2. 定义状态  
100181
112193
113194###### 5. 最终结果  
114195
115- 根据状态定义,最终给结果为  ` dp[n] ` ,即爬到第 ` n `  阶台阶(即楼顶)的方案数为 ` dp[n] ` 。
196+ 根据状态定义,最终结果为  ` dp[n] ` ,即爬到第 ` n `  阶台阶(即楼顶)的方案数为 ` dp[n] ` 。
116197
117- ######  4. 最终结果 
198+ 虽然这道题跟上一道题的状态转移方程都是  ` dp[i] = dp[i - 1] + dp[i - 2] ` ,但是两道题的考察方式并不相同,一定程度上也可以看出来动态规划相关题目的灵活多变。 
118199
119- 根据我们之前定义的状态,` dp[i] `  表示为:以 ` nums[i] `  结尾的最长递增子序列长度。那为了计算出最大的最长递增子序列长度,则需要再遍历一遍 ` dp `  数组,求出最大值即为最终结果。
120- 
121- #### 3.1.4 代码  
200+ #### 3.2.4 代码  
122201
123202``` Python 
124203class  Solution :
@@ -132,35 +211,93 @@ class Solution:
132211        return  dp[n]
133212``` 
134213
135- #### 3.1 .5 复杂度分析  
214+ #### 3.2 .5 复杂度分析  
136215
137216-  ** 时间复杂度** :$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。
138217-  ** 空间复杂度** :$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 ` dp[i] `  的状态只依赖于 ` dp[i - 1] `  和 ` dp[i - 2] ` ,所以可以使用 ` 3 `  个变量来分别表示 ` dp[i] ` 、` dp[i - 1] ` 、` dp[i - 2] ` ,从而将空间复杂度优化到 $O(1)$。
139218
140- ### 3.2 斐波那契数   
219+ ### 3.3 不同路径   
141220
142- #### 3.2 .1 题目链接  
221+ #### 3.3 .1 题目链接  
143222
144- ####  3.2.2 题目大意 
223+ -   [ 62. 不同路径 - 力扣 ] ( https://leetcode.cn/problems/unique-paths/ ) 
145224
146- #### 3.2.3 解题思路   
225+ #### 3.3.2 题目大意   
147226
148- ####  3.2.4 代码 
227+ ** 描述 ** :给定两个整数  ` m `  和  ` n ` ,代表大小为  ` m * n `  的棋盘, 一个机器人位于棋盘左上角的位置,机器人每次只能向右、或者向下移动一步。 
149228
150- ###  3.3 不同路径 
229+ ** 要求 ** :计算出机器人从棋盘左上角到达棋盘右下角一共有多少条不同的路径。 
151230
152- ####  3.3.1 题目链接 
231+ ** 说明 ** : 
153232
154- #### 3.3.2 题目大意  
233+ -  $1 \le m, n \le 100$。
234+ -  题目数据保证答案小于等于 $2 * 10^9$。
235+ 
236+ ** 示例** :
237+ 
238+ ``` Python 
239+ 输入    m =  3 , n =  7 
240+ 输出    28 
241+ ``` 
242+ 
243+ ![ ] ( https://assets.leetcode.com/uploads/2018/10/22/robot_maze.png ) 
155244
156245#### 3.3.3 解题思路  
157246
247+ ###### 1. 划分阶段  
248+ 
249+ 按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。
250+ 
251+ ###### 2. 定义状态  
252+ 
253+ 定义状态 ` dp[i][j] `  为:从左上角到达 ` (i, j) `  位置的路径数量。
254+ 
255+ ###### 3. 状态转移方程  
256+ 
257+ 因为我们每次只能向右、或者向下移动一步,因此想要走到 ` (i, j) ` ,只能从 ` (i - 1, j) `  向下走一步走过来;或者从 ` (i, j - 1) `  向右走一步走过来。所以可以写出状态转移方程为:` dp[i][j] = dp[i - 1][j] + dp[i][j - 1] ` ,此时 ` i > 0,j > 0 ` 。
258+ 
259+ ###### 4. 初始条件  
260+ 
261+ -  从左上角走到 ` (0, 0) `  只有一种方法,即 ` dp[0][0] = 1 ` 。
262+ -  第一行元素只有一条路径(即只能通过前一个元素向右走得到),所以 ` dp[0][j] = 1 ` 。
263+ -  同理,第一列元素只有一条路径(即只能通过前一个元素向下走得到),所以 ` dp[i][0] = 1 ` 。
264+ 
265+ ###### 5. 最终结果  
266+ 
267+ 根据状态定义,最终结果为 ` dp[m - 1][n - 1] ` ,即从左上角到达右下角 ` (m - 1, n - 1) `  位置的路径数量为 ` dp[m - 1][n - 1] ` 。
268+ 
158269#### 3.3.4 代码  
159270
271+ ``` Python 
272+ class  Solution :
273+     def  uniquePaths (self m : int , n : int ) -> int :
274+         dp =  [[0  for  _ in  range (n)] for  _ in  range (m)]
275+         
276+         for  j in  range (n):
277+             dp[0 ][j] =  1 
278+         for  i in  range (m):
279+             dp[i][0 ] =  1 
280+ 
281+         for  i in  range (1 , m):
282+             for  j in  range (1 , n):
283+                 dp[i][j] =  dp[i -  1 ][j] +  dp[i][j -  1 ]
284+         
285+         return  dp[m -  1 ][n -  1 ]
286+ ``` 
287+ 
288+ #### 3.3.5 复杂度分析  
289+ 
290+ -  ** 时间复杂度** :$O(m * n)$。初始条件赋值的时间复杂度为 $O(m + n)$,两重循环遍历的时间复杂度为 $O(m * n)$,所以总体时间复杂度为 $O(m * n)$。
291+ -  ** 空间复杂度** :$O(m * n)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(m * n)$。因为 ` dp[i][j] `  的状态只依赖于上方值 ` dp[i - 1][j] `  和左侧值 ` dp[i][j - 1] ` ,而我们在进行遍历时的顺序刚好是从上至下、从左到右。所以我们可以使用长度为 ` m `  的一维数组来保存状态,从而将空间复杂度优化到 $O(m)$。
292+ 
160293## 参考资料  
161294
162295-  【文章】[ 动态规划基础 - OI Wiki] ( https://oi-wiki.org/dp/basic/ ) 
163296-  【文章】[ 动态规划 1 ——基本概念 - 知乎] ( https://zhuanlan.zhihu.com/p/25441186 ) 
164297-  【文章】[ 动态规划算法 | 曹世宏的博客] ( https://cshihong.github.io/2018/03/30/动态规划算法/ ) 
165298-  【文章】[ 动态规划之初识动规:有了四步解题法模板,再也不害怕动态规划! - 知乎] ( https://zhuanlan.zhihu.com/p/91680256 ) 
166- -  【书籍】
299+ -  【文章】[ 第 6 节 最优子结构、重复子问题、无后效性 | 算法吧] ( https://suanfa8.com/dynamic-programming/06/ ) 
300+ -  【书籍】算法训练营 陈小玉 著
301+ -  【书籍】趣学算法 陈小玉 著
302+ -  【书籍】算法竞赛进阶指南 - 李煜东 著
303+ -  【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编
0 commit comments