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] 就表示两个前缀之间的答案。
字符串 DP:LCS 与编辑距离
一、1143 最长公共子序列 LCS
- 前提条件
给两个字符串:
char* text1;
char* text2;
要求:最长公共子序列长度
注意:子序列可以不连续,但顺序不能变。
例如:
text1 = "abcde"
text2 = "ace"
答案是:3
因为公共子序列是:"ace"
- 二维 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 简化成一维数组
- 为什么能压缩?
二维转移里:
dp[i][j]
只依赖三个位置:
dp[i-1][j] 上方
dp[i][j-1] 左方
dp[i-1][j-1] 左上角
所以我们可以用一维数组:dp[j]
表示:
当前处理到 text1 的某一行时,
text2 前 j 个字符的 LCS 长度。
- 一维 DP 的关键变量
压缩后:dp[j]
在更新前表示:上一行的 dp[i-1][j]
更新后表示:当前行的 dp[i][j]
但是我们还需要:
左上角 dp[i-1][j-1]
所以用一个变量保存:
int pre;
pre 表示:
上一行左上角的值 dp[i-1][j-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;
}
- 这几个变量怎么理解?
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 编辑距离
- 前提条件
给两个字符串:
char* word1;
char* word2;
可以对 word1 做三种操作:
插入一个字符
删除一个字符
替换一个字符
要求:把 word1 变成 word2 的最少操作次数。
五、编辑距离递归思路
- 递归定义
定义:
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 数组
- 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
九、易错点
- 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] 就表示两个前缀之间的答案。