4
4
5
5
> ** 区间动态规划** :线性 DP 的一种,简称为「区间 DP」。以「区间长度」划分阶段,以两个坐标(区间的左、右端点)作为状态的维度。一个状态通常由被它包含且比它更小的区间状态转移而来。
6
6
7
- 区间 DP 的主要思想就是现在小区间内得到最优解 ,再利用小区间的最优解合并,从而得到大区间的最优解,最终得到整个区间的最优解。
7
+ 区间 DP 的主要思想就是:先在小区间内得到最优解 ,再利用小区间的最优解合并,从而得到大区间的最优解,最终得到整个区间的最优解。
8
8
9
- 常见的区间 DP 的状态转移方程一般为:$dp [ i ] [ j ] = max / min \lbrace dp [ i ] [ k ] + dp [ k + 1 ] [ j ] + cost \rbrace,\quad i < k \le j$。
9
+ 根据小区间向大区间转移情况的不同, 常见的区间 DP 问题可以分为两种:
10
10
11
- 1 . 其中状态 $dp[ i] [ j ] $ 表示为:区间 $[ i, j] $ (即下标位置 $i$ 到下标位置 $j$ 上所有元素)上的最大价值。
12
- 2 . $cost$ 表示为:将两个区间 $[ i, k] $ 与 $[ k + 1, j] $ 中所有元素合并起来的代价。
13
- 3 . 这里的 $max / min$ 取决于题目是求最大值还是求最小值。
11
+ 1 . 单个区间从中间向两侧更大区间转移的区间 DP 问题。比如从区间 $[ i + 1, j - 1] $ 转移到更大区间 $[ i, j] $。
12
+ 2 . 多个(大于等于 2ドル$ 个)小区间转移到大区间的区间 DP 问题。比如从区间 $[ i, k] $ 和区间 $[ k, j] $ 转移到区间 $[ i, j] $。
13
+
14
+ 下面我们讲解一下这两种区间 DP 问题的基本解题思路。
14
15
15
16
### 1.2 区间 DP 问题的基本思路
16
17
17
- 区间 DP 的基本解题思路比较固定,具体步骤如下:
18
+ #### 1.2.1 第 1 种区间 DP 问题基本思路
19
+
20
+ 从中间向两侧转移的区间 DP 问题的状态转移方程一般为:$dp[ i] [ j ] = max \lbrace dp[ i + 1] [ j - 1 ] , \quad dp[ i + 1] [ j ] , \quad dp[ i] [ j - 1 ] \rbrace + cost[ i] [ j ] , \quad i \le j$。
21
+
22
+ 1 . 其中 $dp[ i] [ j ] $ 表示为:区间 $[ i, j] $(即下标位置 $i$ 到下标位置 $j$ 上所有元素)上的最大价值。
23
+ 2 . $cost$ 表示为:从小区间转移到区间 $[ i, j] $ 的代价。
24
+ 3 . 这里的 $max / min$ 取决于题目是求最大值还是求最小值。
25
+
26
+ 从中间向两侧转移的区间 DP 问题的基本解题思路如下:
27
+
28
+ 1 . 枚举区间的起点;
29
+ 2 . 枚举区间的终点;
30
+ 3 . 根据状态转移方程计算从小区间转移到更大区间后的最优值。
31
+
32
+ 对应代码如下:
33
+
34
+ ``` Python
35
+ for i in range (size - 1 , - 1 , - 1 ): # 枚举区间起点
36
+ for j in range (i + 1 , size): # 枚举区间终点
37
+ # 状态转移方程,计算转移到更大区间后的最优值
38
+ dp[i][j] = max (dp[i + 1 ][j - 1 ], dp[i + 1 ][j], dp[i][j - 1 ]) + cost[i][j]
39
+ ```
40
+
41
+ #### 1.2.3 第 2 种区间 DP 问题基本思路
42
+
43
+ 多个(大于等于 2ドル$ 个)小区间转移到大区间的区间 DP 问题的状态转移方程一般为:$dp[ i] [ j ] = max / min \lbrace dp[ i] [ k ] + dp[ k + 1] [ j ] + cost[ i] [ j ] \rbrace,\quad i < k \le j$。
44
+
45
+ 1 . 其中状态 $dp[ i] [ j ] $ 表示为:区间 $[ i, j] $ (即下标位置 $i$ 到下标位置 $j$ 上所有元素)上的最大价值。
46
+ 2 . $cost[ i] [ j ] $ 表示为:将两个区间 $[ i, k] $ 与 $[ k + 1, j] $ 中的元素合并为区间 $[ i, j] $ 中的元素的代价。
47
+ 3 . 这里的 $max / min$ 取决于题目是求最大值还是求最小值。
48
+
49
+ 多个小区间转移到大区间的区间 DP 问题的基本解题思路如下:
18
50
19
51
1 . 枚举区间长度;
20
52
2 . 枚举区间的起点,根据区间起点和区间长度得出区间终点;
@@ -38,14 +70,110 @@ for l in range(1, n): # 枚举区间长度
38
70
39
71
下面我们根据几个例子来讲解一下区间 DP 问题的具体解题思路。
40
72
41
- ### 2.1 戳气球
73
+ ### 2.1 最长回文子序列
42
74
43
75
#### 2.1.1 题目链接
44
76
45
- - [ 312. 戳气球 - 力扣] ( https://leetcode.cn/problems/burst-balloons / )
77
+ - [ 516. 最长回文子序列 - 力扣] ( https://leetcode.cn/problems/longest-palindromic-subsequence / )
46
78
47
79
#### 2.1.2 题目大意
48
80
81
+ ** 描述** :给定一个字符串 $s$。
82
+
83
+ ** 要求** :找出其中最长的回文子序列,并返回该序列的长度。
84
+
85
+ ** 说明** :
86
+
87
+ - ** 子序列** :不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
88
+ - 1ドル \le s.length \le 1000$。
89
+ - $s$ 仅由小写英文字母组成。
90
+
91
+ ** 示例** :
92
+
93
+ - 示例 1:
94
+
95
+ ``` Python
96
+ 输入:s = " bbbab"
97
+ 输出:4
98
+ 解释:一个可能的最长回文子序列为 " bbbb" 。
99
+ ```
100
+
101
+ - 示例 2:
102
+
103
+ ``` Python
104
+ 输入:s = " cbbd"
105
+ 输出:2
106
+ 解释:一个可能的最长回文子序列为 " bb" 。
107
+ ```
108
+
109
+ #### 2.1.3 解题思路
110
+
111
+ ##### 思路 1:动态规划
112
+
113
+ ###### 1. 划分阶段
114
+
115
+ 按照区间长度进行阶段划分。
116
+
117
+ ###### 2. 定义状态
118
+
119
+ 定义状态 $dp[ i] [ j ] $ 表示为:字符串 $s$ 在区间 $[ i, j] $ 范围内的最长回文子序列长度。
120
+
121
+ ###### 3. 状态转移方程
122
+
123
+ 我们对区间 $[ i, j] $ 边界位置上的字符 $s[ i] $ 与 $s[ j] $ 进行分类讨论:
124
+
125
+ 1 . 如果 $s[ i] = s[ j] ,ドル则 $dp[ i] [ j ] $ 为区间 $[ i + 1, j - 1] $ 范围内最长回文子序列长度 + 2ドル,ドル即 $dp[ i] [ j ] = dp[ i + 1] [ j - 1 ] + 2$。
126
+ 2 . 如果 $s[ i] \ne s[ j] ,ドル则 $dp[ i] [ j ] $ 取决于以下两种情况,取其最大的一种:
127
+ 1. 加入 $s[i]$ 所能组成的最长回文子序列长度,即:$dp[i][j] = dp[i][j - 1]$。
128
+ 2. 加入 $s[j]$ 所能组成的最长回文子序列长度,即:$dp[i][j] = dp[i - 1][j]$。
129
+
130
+ 则状态转移方程为:
131
+
132
+ $dp[ i] [ j ] = \begin{cases} max \lbrace dp[ i + 1] [ j - 1 ] + 2 \rbrace & s[ i] = s[ j] \cr max \lbrace dp[ i] [ j - 1 ] , dp[ i - 1] [ j ] \rbrace & s[ i] \ne s[ j] \end{cases}$
133
+
134
+ ###### 4. 初始条件
135
+
136
+ - 单个字符的最长回文序列是 1ドル,ドル即 $dp[ i] [ i ] = 1$。
137
+
138
+ ###### 5. 最终结果
139
+
140
+ 由于 $dp[ i] [ j ] $ 依赖于 $dp[ i + 1] [ j - 1 ] $、$dp[ i + 1] [ j ] $、$dp[ i] [ j - 1 ] ,ドル所以我们应该按照从下到上、从左到右的顺序进行遍历。
141
+
142
+ 根据我们之前定义的状态,$dp[ i] [ j ] $ 表示为:字符串 $s$ 在区间 $[ i, j] $ 范围内的最长回文子序列长度。所以最终结果为 $dp[ 0] [ size - 1 ] $。
143
+
144
+ ##### 思路 1:代码
145
+
146
+ ``` Python
147
+ class Solution :
148
+ def longestPalindromeSubseq (self , s : str ) -> int :
149
+ size = len (s)
150
+ dp = [[0 for _ in range (size)] for _ in range (size)]
151
+ for i in range (size):
152
+ dp[i][i] = 1
153
+
154
+ for i in range (size - 1 , - 1 , - 1 ):
155
+ for j in range (i + 1 , size):
156
+ if s[i] == s[j]:
157
+ dp[i][j] = dp[i + 1 ][j - 1 ] + 2
158
+ else :
159
+ dp[i][j] = max (dp[i + 1 ][j], dp[i][j - 1 ])
160
+
161
+ return dp[0 ][size - 1 ]
162
+ ```
163
+
164
+ ##### 思路 1:复杂度分析
165
+
166
+ - ** 时间复杂度** :$O(n^2),ドル其中 $n$ 为字符串 $s$ 的长度。
167
+ - ** 空间复杂度** :$O(n^2)$。
168
+
169
+ ### 2.2 戳气球
170
+
171
+ #### 2.2.1 题目链接
172
+
173
+ - [ 312. 戳气球 - 力扣] ( https://leetcode.cn/problems/burst-balloons/ )
174
+
175
+ #### 2.2.2 题目大意
176
+
49
177
** 描述** :有 $n$ 个气球,编号为 0ドル \sim n - 1,ドル每个气球上都有一个数字,这些数字存在数组 $nums$ 中。现在开始戳破气球。其中戳破第 $i$ 个气球,可以获得 $nums[ i - 1] \times nums[ i] \times nums[ i + 1] $ 枚硬币,这里的 $i - 1$ 和 $i + 1$ 代表和 $i$ 相邻的两个气球的编号。如果 $i - 1$ 或 $i + 1$ 超出了数组的边界,那么就当它是一个数字为 1ドル$ 的气球。
50
178
51
179
** 要求** :求出能获得硬币的最大数量。
@@ -78,7 +206,7 @@ nums = [1,5] --> [5] --> []
78
206
coins = 1 * 1 * 5 + 1 * 5 * 1 = 10
79
207
```
80
208
81
- #### 2.1 .3 解题思路
209
+ #### 2.2 .3 解题思路
82
210
83
211
##### 思路 1:动态规划
84
212
@@ -108,7 +236,7 @@ $dp[i][j] = max \lbrace dp[i][k] + dp[k][j] + nums[i] \times nums[k] \times nums
108
236
109
237
###### 5. 最终结果
110
238
111
- 根据我们之前定义的状态,$dp[ i] [ j ] $ 表示为:戳破所有气球 $i$ 与气球 $j$ 之间的气球(不包含气球 $i$ 和 气球 $j$),所能获取的最多硬币数。。 所以最终结果为 $dp[ 0] [ n + 1 ] $。
239
+ 根据我们之前定义的状态,$dp[ i] [ j ] $ 表示为:戳破所有气球 $i$ 与气球 $j$ 之间的气球(不包含气球 $i$ 和 气球 $j$),所能获取的最多硬币数。所以最终结果为 $dp[ 0] [ n + 1 ] $。
112
240
113
241
##### 思路 1:代码
114
242
@@ -139,22 +267,114 @@ class Solution:
139
267
- ** 时间复杂度** :$O(n^3),ドル其中 $n$ 为气球数量。
140
268
- ** 空间复杂度** :$O(n^2)$。
141
269
142
- ### 2.2 切棍子的最小成本
270
+ ### 2.3 切棍子的最小成本
143
271
144
- #### 2.2 .1 题目链接
272
+ #### 2.3 .1 题目链接
145
273
146
274
- [ 1547. 切棍子的最小成本 - 力扣] ( https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/ )
147
275
148
- #### 2.2 .2 题目大意
276
+ #### 2.3 .2 题目大意
149
277
150
- #### 2.2.3 解题思路
278
+ ** 描述 ** :给定一个整数 $n,ドル代表一根长度为 $n$ 个单位的木根,木棍从 0ドル \sim n$ 标记了若干位置。例如,长度为 6ドル$ 的棍子可以标记如下:
151
279
152
- ### 2.3 最长回文子串
280
+ ![ ] ( https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/09/statement.jpg )
153
281
154
- #### 2.3.1 题目链接
282
+ 再给定一个整数数组 $cuts,ドル其中 $cuts [ i ] $ 表示需要将棍子切开的位置。
155
283
156
- - [ 5. 最长回文子串 - 力扣 ] ( https://leetcode.cn/problems/longest-palindromic-substring/ )
284
+ 我们可以按照顺序完成切割,也可以根据需要更改切割顺序。
157
285
158
- #### 2.3.2 题目大意
286
+ 每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是所有次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根小木棍的长度和就是切割前木棍的长度)。
287
+
288
+ ** 要求** :返回切棍子的最小总成本。
289
+
290
+ ** 说明** :
291
+
292
+ - 2ドル \le n \le 10^6$。
293
+ - 1ドル \le cuts.length \le min(n - 1, 100)$。
294
+ - 1ドル \le cuts[ i] \le n - 1$。
295
+ - $cuts$ 数组中的所有整数都互不相同。
296
+
297
+ ** 示例** :
298
+
299
+ - 示例 1:
300
+
301
+ ![ ] ( https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/09/e1.jpg )
302
+
303
+ ``` Python
304
+ 输入:n = 7 , cuts = [1 ,3 ,4 ,5 ]
305
+ 输出:16
306
+ 解释:按 [1 , 3 , 4 , 5 ] 的顺序切割的情况如下所示。
307
+ 第一次切割长度为 7 的棍子,成本为 7 。第二次切割长度为 6 的棍子(即第一次切割得到的第二根棍子),第三次切割为长度 4 的棍子,最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。而将切割顺序重新排列为 [3 , 5 , 1 , 4 ] 后,总成本 = 16 (如示例图中 7 + 4 + 3 + 2 = 16 )。
308
+ ```
309
+
310
+ ![ ] ( https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/09/e11.jpg )
311
+
312
+ - 示例 2:
313
+
314
+ ``` Python
315
+ 输入:n = 9 , cuts = [5 ,6 ,1 ,4 ,2 ]
316
+ 输出:22
317
+ 解释:如果按给定的顺序切割,则总成本为 25 。总成本 <= 25 的切割顺序很多,例如,[4 , 6 , 5 , 2 , 1 ] 的总成本 = 22 ,是所有可能方案中成本最小的。
318
+ ```
319
+
320
+ #### 2.3.3 解题思路
321
+
322
+ ##### 思路 1:动态规划
323
+
324
+ 我们可以预先在数组 $cuts$ 种添加位置 0ドル$ 和位置 $n,ドル然后对数组 $cuts$ 进行排序。这样待切割的木棍就对应了数组中连续元素构成的「区间」。
325
+
326
+ ###### 1. 划分阶段
327
+
328
+ 按照区间长度进行阶段划分。
329
+
330
+ ###### 2. 定义状态
331
+
332
+ 定义状态 $dp[ i] [ j ] $ 表示为:切割区间为 $[ i, j] $ 上的小木棍的最小成本。
333
+
334
+ ###### 3. 状态转移方程
335
+
336
+ 假设位置 $i$ 与位置 $j$ 之间最后一个切割的位置为 $k,ドル则 $dp[ i] [ j ] $ 取决与由 $k$ 作为切割点分割出的两个区间 $[ i, k] $ 与 $[ k, j] $ 上的最小成本 + 切割位置 $k$ 所带来的成本。
337
+
338
+ 而切割位置 $k$ 所带来的成本是这段区间所代表的小木棍的长度,即 $cuts[ j] - cuts[ i] $。
339
+
340
+ 则状态转移方程为:$dp[ i] [ j ] = min \lbrace dp[ i] [ k ] + dp[ k] [ j ] + cuts[ j] - cuts[ i] \rbrace, \quad i < k < j$
341
+
342
+ ###### 4. 初始条件
343
+
344
+ - 相邻位置之间没有切割点,不需要切割,最小成本为 0ドル,ドル即 $dp[ i - 1] [ i ] = 0$。
345
+ - 其余位置默认为最小成本为一个极大值,即 $dp[ i] [ j ] = \infty, \quad i + 1 \ne j$。
346
+
347
+ ###### 5. 最终结果
348
+
349
+ 根据我们之前定义的状态,$dp[ i] [ j ] $ 表示为:切割区间为 $[ i, j] $ 上的小木棍的最小成本。 所以最终结果为 $dp[ 0] [ size - 1 ] $。
350
+
351
+ ##### 思路 1:代码
352
+
353
+ ``` Python
354
+ class Solution :
355
+ def minCost (self , n : int , cuts : List[int ]) -> int :
356
+ cuts.append(0 )
357
+ cuts.append(n)
358
+ cuts.sort()
359
+
360
+ size = len (cuts)
361
+ dp = [[float (' inf' ) for _ in range (size)] for _ in range (size)]
362
+ for i in range (1 , size):
363
+ dp[i - 1 ][i] = 0
364
+
365
+ for l in range (3 , size + 1 ): # 枚举区间长度
366
+ for i in range (size): # 枚举区间起点
367
+ j = i + l - 1 # 根据起点和长度得到终点
368
+ if j >= size:
369
+ continue
370
+ dp[i][j] = float (' inf' )
371
+ for k in range (i + 1 , j): # 枚举区间分割点
372
+ # 状态转移方程,计算合并区间后的最优值
373
+ dp[i][j] = min (dp[i][j], dp[i][k] + dp[k][j] + cuts[j] - cuts[i])
374
+ return dp[0 ][size - 1 ]
375
+ ```
376
+
377
+ ##### 思路 1:复杂度分析
159
378
160
- #### 2.3.3 解题思路
379
+ - ** 时间复杂度** :$O(m^3),ドル其中 $m$ 为数组 $cuts$ 的元素个数。
380
+ - ** 空间复杂度** :$O(m^2)$。
0 commit comments