返回 LeetCode 刷题

Markdown File

线性DP习题

线性DP习题.md

1字符串 DP:LCS 与编辑距离
2
3一、1143 最长公共子序列 LCS
41. 前提条件
5
6给两个字符串:
7
8char* text1;
9char* text2;
10
11要求:最长公共子序列长度
12
13注意:子序列可以不连续,但顺序不能变。
14
15例如:
16
17text1 = "abcde"
18text2 = "ace"
19
20答案是:3
21因为公共子序列是:"ace"
22
232. 二维 DP 回顾
24状态定义:
25dp[i][j]
26
27表示:
28
29text1 前 i 个字符和text2 前 j 个字符的最长公共子序列长度。
30
31转移:
32
33如果 text1[i-1] == text2[j-1]:
34
35 dp[i][j] = dp[i-1][j-1] + 1
36
37否则:
38
39 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
40
41二、LCS 简化成一维数组
421. 为什么能压缩?
43
44二维转移里:
45
46dp[i][j]
47
48只依赖三个位置:
49
50dp[i-1][j] 上方
51dp[i][j-1] 左方
52dp[i-1][j-1] 左上角
53
54所以我们可以用一维数组:dp[j]
55
56表示:
57当前处理到 text1 的某一行时,
58text2 前 j 个字符的 LCS 长度。
59
602. 一维 DP 的关键变量
61
62压缩后:dp[j]
63
64在更新前表示:上一行的 dp[i-1][j]
65
66更新后表示:当前行的 dp[i][j]
67
68但是我们还需要:
69
70左上角 dp[i-1][j-1]
71
72所以用一个变量保存:
73
74int pre;
75
76pre 表示:
77
78上一行左上角的值 dp[i-1][j-1]
79
803. LCS 一维 DP 代码
81#include <stdlib.h>
82#include <string.h>
83
84int max(int a, int b) {
85 return a > b ? a : b;
86}
87
88int longestCommonSubsequence(char* text1, char* text2) {
89 int m = strlen(text1);
90 int n = strlen(text2);
91
92 int* dp = malloc((n + 1) * sizeof(int));
93
94 for (int j = 0; j <= n; j++) {
95 dp[j] = 0;
96 }
97
98 for (int i = 1; i <= m; i++) {
99 int pre = 0;
100
101 for (int j = 1; j <= n; j++) {
102 int temp = dp[j];
103
104 if (text1[i - 1] == text2[j - 1]) {
105 dp[j] = pre + 1;
106 } else {
107 dp[j] = max(dp[j], dp[j - 1]);
108 }
109//相当于pre用的是上一次被覆盖前的值,需要保留下来
110 pre = temp;
111 }
112 }
113
114 int ans = dp[n];
115
116 free(dp);
117
118 return ans;
119}
120
1214. 这几个变量怎么理解?
122int temp = dp[j];
123
124保存的是:还没更新前的 dp[j]
125也就是上一行的 dp[i-1][j]
126
127pre
128保存的是:
129
130上一行左上角 dp[i-1][j-1]
131dp[j - 1]
132
133因为当前行从左到右更新,所以它表示:
134
135当前行左边 dp[i][j-1]
136
137所以:
138
139dp[j] 更新前:dp[i-1][j]
140dp[j-1] 已更新:dp[i][j-1]
141pre 左上角:dp[i-1][j-1]
142
143三、LCS 一维例题
144text1 = "abc"
145text2 = "ac"
146
147答案应该是:2
148
149公共子序列:"ac"
150初始
151text2 = "" a c
152dp = 0 0 0
153i = 1,text1[i-1] = 'a'
154
155初始:
156
157pre = 0
158j = 1,text2[j-1] = 'a'
159
160相等:
161
162dp[1] = pre + 1 = 1
163
164更新后:
165
166dp = [0, 1, 0]
167j = 2,text2[j-1] = 'c'
168
169不等:
170
171dp[2] = max(dp[2], dp[1]) = max(0,1) = 1
172
173更新后:
174
175dp = [0, 1, 1]
176i = 2,text1[i-1] = 'b'
177
178和 a、c 都不等。
179
180最终:
181
182dp = [0, 1, 1]
183i = 3,text1[i-1] = 'c'
184j = 1,text2[j-1] = 'a'
185
186不等:
187dp[1] = max(dp[1], dp[0]) = 1
188j = 2,text2[j-1] = 'c'
189
190相等:dp[2] = pre + 1
191
192这里 pre 是上一行左上角:
193
194dp[i-1][j-1] = dp[2][1] = 1
195
196所以:dp[2] = 2
197
198最终:
199
200dp = [0, 1, 2]
201
202答案:dp[2] = 2
203
204四、72 编辑距离
2051. 前提条件
206
207给两个字符串:
208
209char* word1;
210char* word2;
211
212可以对 word1 做三种操作:
213
214插入一个字符
215删除一个字符
216替换一个字符
217
218要求:把 word1 变成 word2 的最少操作次数。
219
220五、编辑距离递归思路
2211. 递归定义
222
223定义:
224
225dfs(i, j)
226
227表示:
228
229把 word1 前 i 个字符
230变成 word2 前 j 个字符
231
232最少需要多少次操作。
2332. 当前字符相等
234
235如果:word1[i - 1] == word2[j - 1]
236
237最后一个字符已经一样,不需要操作:
238
239dfs(i, j) = dfs(i - 1, j - 1)
2403. 当前字符不相等
241
242如果:
243
244word1[i - 1] != word2[j - 1]
245
246有三种操作。
247
248操作一:删除
249
250删除 word1[i-1]:
251
252dfs(i - 1, j) + 1
253操作二:插入
254
255在 word1 后面插入 word2[j-1]:
256
257dfs(i, j - 1) + 1
258
259为什么是 dfs(i, j-1)?
260
261因为插入后,word2[j-1] 已经匹配掉了,接下来只需要把:
262
263word1 前 i 个字符
264变成
265word2 前 j-1 个字符
266操作三:替换
267
268把 word1[i-1] 替换成 word2[j-1]:
269
270dfs(i - 1, j - 1) + 1
2714. 递归转移
272如果 word1[i-1] == word2[j-1]:
273
274 dfs(i,j) = dfs(i-1,j-1)
275
276否则:
277
278 dfs(i,j) =
279 min(
280 dfs(i-1,j),
281 dfs(i,j-1),
282 dfs(i-1,j-1)
283 ) + 1
284六、编辑距离 DP 数组
2851. DP 定义
286dp[i][j]
287
288表示:
289
290把 word1 前 i 个字符
291变成 word2 前 j 个字符
292
293的最少操作次数。
2942. 初始化
295dp[0][j] = j
296
297意思是:
298
299word1 是空串,
300要变成 word2 前 j 个字符,
301只能插入 j 次。
302dp[i][0] = i
303
304意思是:
305
306word2 是空串,
307word1 前 i 个字符要变成空串,
308只能删除 i 次。
3093. DP 转移
310如果 word1[i-1] == word2[j-1]:
311
312 dp[i][j] = dp[i-1][j-1]
313
314否则:
315
316 dp[i][j] =
317 min(
318 dp[i-1][j], 删除
319 dp[i][j-1], 插入
320 dp[i-1][j-1] 替换
321 ) + 1
3224. 编辑距离 C 代码
323#include <stdlib.h>
324#include <string.h>
325
326int min3(int a, int b, int c) {
327 int m = a < b ? a : b;
328 return m < c ? m : c;
329}
330
331int minDistance(char* word1, char* word2) {
332 int m = strlen(word1);
333 int n = strlen(word2);
334
335 int** dp = malloc((m + 1) * sizeof(int*));
336
337 for (int i = 0; i <= m; i++) {
338 dp[i] = malloc((n + 1) * sizeof(int));
339 }
340
341 for (int i = 0; i <= m; i++) {
342 dp[i][0] = i;
343 }
344
345 for (int j = 0; j <= n; j++) {
346 dp[0][j] = j;
347 }
348
349 for (int i = 1; i <= m; i++) {
350 for (int j = 1; j <= n; j++) {
351 if (word1[i - 1] == word2[j - 1]) {
352 dp[i][j] = dp[i - 1][j - 1];
353 } else {
354 dp[i][j] = min3(
355 dp[i - 1][j],
356 dp[i][j - 1],
357 dp[i - 1][j - 1]
358 ) + 1;
359 }
360 }
361 }
362
363 int ans = dp[m][n];
364
365 for (int i = 0; i <= m; i++) {
366 free(dp[i]);
367 }
368
369 free(dp);
370
371 return ans;
372}
373七、编辑距离例题
374word1 = "horse"
375word2 = "ros"
376
377答案是:
378
3793
380
381一种操作:
382
383horse -> rorse 替换 h 为 r
384rorse -> rose 删除 r
385rose -> ros 删除 e
386DP 表含义
387
388行是 word1 前 i 个字符:
389
390"" h o r s e
391
392列是 word2 前 j 个字符:
393
394"" r o s
395
396最终求:
397
398dp[5][3]
399
400也就是:
401
402horse -> ros
403初始化
404 "" r o s
405"" 0 1 2 3
406h 1
407o 2
408r 3
409s 4
410e 5
411填表规则
412
413如果字符相等:
414
415直接看左上角
416
417如果字符不等:
418
419从 上、左、左上 三个位置取最小,再 +1
420
421最终表:
422
423 "" r o s
424"" 0 1 2 3
425h 1 1 2 3
426o 2 2 1 2
427r 3 2 2 2
428s 4 3 3 2
429e 5 4 4 3
430
431答案:
432
433dp[5][3] = 3
434八、LCS 和编辑距离对比
435题目 dp[i][j] 含义 相等时 不等时
436LCS 两个前缀的最长公共子序列长度 dp[i-1][j-1]+1 max(dp[i-1][j], dp[i][j-1])
437编辑距离 word1 前 i 个变 word2 前 j 个的最少操作数 dp[i-1][j-1] min(上, 左, 左上)+1
438九、易错点
4391. dp[i][j] 对应的是前 i 个字符
440
441所以访问字符串时:
442
443word1[i - 1]
444word2[j - 1]
4452. LCS 是求最大长度
446
447所以用:
448
449max(...)
4503. 编辑距离是求最小操作次数
451
452所以用:
453
454min(...)
4554. 编辑距离初始化很重要
456dp[0][j] = j
457dp[i][0] = i
458
459因为空串变非空串只能插入,非空串变空串只能删除。
460
461十、总结
462字符串 DP 总结
463
464--------------------------------
4651143 最长公共子序列
466--------------------------------
467
468dp[i][j]:
469
470text1 前 i 个字符
471和 text2 前 j 个字符
472的最长公共子序列长度。
473
474如果 text1[i-1] == text2[j-1]:
475
476 dp[i][j] = dp[i-1][j-1] + 1
477
478否则:
479
480 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
481
482一维优化:
483
484dp[j] 表示当前行的 dp[i][j]
485
486pre 表示左上角 dp[i-1][j-1]
487
488temp 保存更新前的 dp[j]
489
490--------------------------------
49172 编辑距离
492--------------------------------
493
494dp[i][j]:
495
496word1 前 i 个字符
497变成 word2 前 j 个字符
498的最少操作数。
499
500如果 word1[i-1] == word2[j-1]:
501
502 dp[i][j] = dp[i-1][j-1]
503
504否则:
505
506 dp[i][j] =
507 min(
508 dp[i-1][j], 删除
509 dp[i][j-1], 插入
510 dp[i-1][j-1] 替换
511 ) + 1
512
513初始化:
514
515dp[0][j] = j
516dp[i][0] = i
517
518字符串 DP 通常是在比较两个前缀,dp[i][j] 就表示两个前缀之间的答案。
Rendered Preview

字符串 DP:LCS 与编辑距离

一、1143 最长公共子序列 LCS

  1. 前提条件

给两个字符串:

char* text1;
char* text2;

要求:最长公共子序列长度

注意:子序列可以不连续,但顺序不能变。

例如:

text1 = "abcde"
text2 = "ace"

答案是:3
因为公共子序列是:"ace"

  1. 二维 DP 回顾
    状态定义:
    dp[i][j]

表示:

text1 前 i 个字符和text2 前 j 个字符的最长公共子序列长度。

转移:

如果 text1[i-1] == text2[j-1]:

dp[i][j] = dp[i-1][j-1] + 1

否则:

dp[i][j] = max(dp[i-1][j], dp[i][j-1])

二、LCS 简化成一维数组

  1. 为什么能压缩?

二维转移里:

dp[i][j]

只依赖三个位置:

dp[i-1][j] 上方
dp[i][j-1] 左方
dp[i-1][j-1] 左上角

所以我们可以用一维数组:dp[j]

表示:
当前处理到 text1 的某一行时,
text2 前 j 个字符的 LCS 长度。

  1. 一维 DP 的关键变量

压缩后:dp[j]

在更新前表示:上一行的 dp[i-1][j]

更新后表示:当前行的 dp[i][j]

但是我们还需要:

左上角 dp[i-1][j-1]

所以用一个变量保存:

int pre;

pre 表示:

上一行左上角的值 dp[i-1][j-1]

  1. LCS 一维 DP 代码
    #include <stdlib.h>
    #include <string.h>

int max(int a, int b) {
return a > b ? a : b;
}

int longestCommonSubsequence(char* text1, char* text2) {
int m = strlen(text1);
int n = strlen(text2);

int* dp = malloc((n + 1) * sizeof(int));

for (int j = 0; j <= n; j++) {
    dp[j] = 0;
}

for (int i = 1; i <= m; i++) {
    int pre = 0;

    for (int j = 1; j <= n; j++) {
        int temp = dp[j];

        if (text1[i - 1] == text2[j - 1]) {
            dp[j] = pre + 1;
        } else {
            dp[j] = max(dp[j], dp[j - 1]);
        }

//相当于pre用的是上一次被覆盖前的值,需要保留下来
pre = temp;
}
}

int ans = dp[n];

free(dp);

return ans;

}

  1. 这几个变量怎么理解?
    int temp = dp[j];

保存的是:还没更新前的 dp[j]
也就是上一行的 dp[i-1][j]

pre
保存的是:

上一行左上角 dp[i-1][j-1]
dp[j - 1]

因为当前行从左到右更新,所以它表示:

当前行左边 dp[i][j-1]

所以:

dp[j] 更新前:dp[i-1][j]
dp[j-1] 已更新:dp[i][j-1]
pre 左上角:dp[i-1][j-1]

三、LCS 一维例题
text1 = "abc"
text2 = "ac"

答案应该是:2

公共子序列:"ac"
初始
text2 = "" a c
dp = 0 0 0
i = 1,text1[i-1] = 'a'

初始:

pre = 0
j = 1,text2[j-1] = 'a'

相等:

dp[1] = pre + 1 = 1

更新后:

dp = [0, 1, 0]
j = 2,text2[j-1] = 'c'

不等:

dp[2] = max(dp[2], dp[1]) = max(0,1) = 1

更新后:

dp = [0, 1, 1]
i = 2,text1[i-1] = 'b'

和 a、c 都不等。

最终:

dp = [0, 1, 1]
i = 3,text1[i-1] = 'c'
j = 1,text2[j-1] = 'a'

不等:
dp[1] = max(dp[1], dp[0]) = 1
j = 2,text2[j-1] = 'c'

相等:dp[2] = pre + 1

这里 pre 是上一行左上角:

dp[i-1][j-1] = dp[2][1] = 1

所以:dp[2] = 2

最终:

dp = [0, 1, 2]

答案:dp[2] = 2

四、72 编辑距离

  1. 前提条件

给两个字符串:

char* word1;
char* word2;

可以对 word1 做三种操作:

插入一个字符
删除一个字符
替换一个字符

要求:把 word1 变成 word2 的最少操作次数。

五、编辑距离递归思路

  1. 递归定义

定义:

dfs(i, j)

表示:

把 word1 前 i 个字符
变成 word2 前 j 个字符

最少需要多少次操作。
2. 当前字符相等

如果:word1[i - 1] == word2[j - 1]

最后一个字符已经一样,不需要操作:

dfs(i, j) = dfs(i - 1, j - 1)
3. 当前字符不相等

如果:

word1[i - 1] != word2[j - 1]

有三种操作。

操作一:删除

删除 word1[i-1]:

dfs(i - 1, j) + 1
操作二:插入

在 word1 后面插入 word2[j-1]:

dfs(i, j - 1) + 1

为什么是 dfs(i, j-1)?

因为插入后,word2[j-1] 已经匹配掉了,接下来只需要把:

word1 前 i 个字符
变成
word2 前 j-1 个字符
操作三:替换

把 word1[i-1] 替换成 word2[j-1]:

dfs(i - 1, j - 1) + 1
4. 递归转移
如果 word1[i-1] == word2[j-1]:

dfs(i,j) = dfs(i-1,j-1)

否则:

dfs(i,j) =
min(
    dfs(i-1,j),
    dfs(i,j-1),
    dfs(i-1,j-1)
) + 1

六、编辑距离 DP 数组

  1. DP 定义
    dp[i][j]

表示:

把 word1 前 i 个字符
变成 word2 前 j 个字符

的最少操作次数。
2. 初始化
dp[0][j] = j

意思是:

word1 是空串,
要变成 word2 前 j 个字符,
只能插入 j 次。
dp[i][0] = i

意思是:

word2 是空串,
word1 前 i 个字符要变成空串,
只能删除 i 次。
3. DP 转移
如果 word1[i-1] == word2[j-1]:

dp[i][j] = dp[i-1][j-1]

否则:

dp[i][j] =
min(
    dp[i-1][j],      删除
    dp[i][j-1],      插入
    dp[i-1][j-1]     替换
) + 1

4. 编辑距离 C 代码
#include <stdlib.h>
#include <string.h>

int min3(int a, int b, int c) {
int m = a < b ? a : b;
return m < c ? m : c;
}

int minDistance(char* word1, char* word2) {
int m = strlen(word1);
int n = strlen(word2);

int** dp = malloc((m + 1) * sizeof(int*));

for (int i = 0; i <= m; i++) {
    dp[i] = malloc((n + 1) * sizeof(int));
}

for (int i = 0; i <= m; i++) {
    dp[i][0] = i;
}

for (int j = 0; j <= n; j++) {
    dp[0][j] = j;
}

for (int i = 1; i <= m; i++) {
    for (int j = 1; j <= n; j++) {
        if (word1[i - 1] == word2[j - 1]) {
            dp[i][j] = dp[i - 1][j - 1];
        } else {
            dp[i][j] = min3(
                dp[i - 1][j],
                dp[i][j - 1],
                dp[i - 1][j - 1]
            ) + 1;
        }
    }
}

int ans = dp[m][n];

for (int i = 0; i <= m; i++) {
    free(dp[i]);
}

free(dp);

return ans;

}
七、编辑距离例题
word1 = "horse"
word2 = "ros"

答案是:

3

一种操作:

horse -> rorse 替换 h 为 r
rorse -> rose 删除 r
rose -> ros 删除 e
DP 表含义

行是 word1 前 i 个字符:

"" h o r s e

列是 word2 前 j 个字符:

"" r o s

最终求:

dp[5][3]

也就是:

horse -> ros
初始化
"" r o s
"" 0 1 2 3
h 1
o 2
r 3
s 4
e 5
填表规则

如果字符相等:

直接看左上角

如果字符不等:

从 上、左、左上 三个位置取最小,再 +1

最终表:

  ""  r  o  s

"" 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3

答案:

dp[5][3] = 3
八、LCS 和编辑距离对比
题目 dp[i][j] 含义 相等时 不等时
LCS 两个前缀的最长公共子序列长度 dp[i-1][j-1]+1 max(dp[i-1][j], dp[i][j-1])
编辑距离 word1 前 i 个变 word2 前 j 个的最少操作数 dp[i-1][j-1] min(上, 左, 左上)+1
九、易错点

  1. dp[i][j] 对应的是前 i 个字符

所以访问字符串时:

word1[i - 1]
word2[j - 1]
2. LCS 是求最大长度

所以用:

max(...)
3. 编辑距离是求最小操作次数

所以用:

min(...)
4. 编辑距离初始化很重要
dp[0][j] = j
dp[i][0] = i

因为空串变非空串只能插入,非空串变空串只能删除。

十、总结
字符串 DP 总结


1143 最长公共子序列

dp[i][j]:

text1 前 i 个字符
和 text2 前 j 个字符
的最长公共子序列长度。

如果 text1[i-1] == text2[j-1]:

dp[i][j] = dp[i-1][j-1] + 1

否则:

dp[i][j] = max(dp[i-1][j], dp[i][j-1])

一维优化:

dp[j] 表示当前行的 dp[i][j]

pre 表示左上角 dp[i-1][j-1]

temp 保存更新前的 dp[j]


72 编辑距离

dp[i][j]:

word1 前 i 个字符
变成 word2 前 j 个字符
的最少操作数。

如果 word1[i-1] == word2[j-1]:

dp[i][j] = dp[i-1][j-1]

否则:

dp[i][j] =
min(
    dp[i-1][j],      删除
    dp[i][j-1],      插入
    dp[i-1][j-1]     替换
) + 1

初始化:

dp[0][j] = j
dp[i][0] = i

字符串 DP 通常是在比较两个前缀,dp[i][j] 就表示两个前缀之间的答案。