算法复习——动态规划篇之编辑距离问题
以下内容主要参考中国大学MOOC《算法设计与分析》,墙裂推荐希望入门算法的童鞋学习!
1. 问题背景
在输入法自动更正时,像kittchen、kithen和kitchem等会被自动更正为kitchen,而不会被更正为sitting等其他单词。那么,问题就是如何衡量序列的相似程度?
我们可以用编辑操作来度量两个序列的相似程度。一个字符串A可以通过一系列编辑操作转变成另一个字符串B,假设我们只允许三种编辑操作:删除、插入和替换。如下图所示,kittchen、kithen和kitchem等都只需要通过一次操作就可以变成kitchen。
那么,像kittchen要经过几次编辑操作会变成sitting呢?比如,有以下两种方案。
问题在于如何求出最少的编辑操作数(最小编辑距离)?
2. 问题定义
编辑距离问题(Minimum Edit Distance, MED)
输入:
- 长度为 n n n的字符串 s s s,长度为 m m m的字符串 t t t
输出:
-
求出一组编辑操作 O = < e 1 , e 2 , … , e d > O=<e_1, e_2, \dots, e_d> O=<e1,e2,…,ed>,令 m i n ∣ O ∣ min|O| min∣O∣,
s . t . s.t. s.t.字符串 s s s经过 O O O的操作后满足 s = t s=t s=t
3. 动态规划
3.1 问题结构分析
-
给出问题表示
- D [ i , j ] D[i, j] D[i,j]:字符串 s [ 1.. i ] s[1..i] s[1..i]变为 t [ 1.. j ] t[1..j] t[1..j]的最小编辑距离
-
明确原始问题
- D [ n , m ] D[n, m] D[n,m]:字符串 s [ 1.. n ] s[1..n] s[1..n]变为 t [ 1.. m ] t[1..m] t[1..m]的最小编辑距离
3.2 递推关系建立
3.2.1 分析最优(子)结构
在这个问题中,我们仍然考察的是末尾元素,但是我们只关心删除、插入和替换来操作 s s s串。
删除是删除 s s s串的最后一位,这是有助于我们从 s s s串变成 t t t串的,如下图所示,所以我们进一步需要关心的是如何从 s [ 1.. i − 1 ] s[1..i-1] s[1..i−1]变成 t [ 1.. j ] t[1..j] t[1..j],因此 D [ i , j ] = D [ i − 1 , j ] + 1 D[i, j] = D[i-1, j] + 1 D[i,j]=D[i−1,j]+1。
插入是在 s s s串的最后插入一位,且最后插入的元素一定是 t t t串的最后一位,如下图所示,所以我们进一步需要关心的是如何从 s [ 1.. i ] s[1..i] s[1..i]变成 t [ 1.. j − 1 ] t[1..j-1] t[1..j−1],因此 D [ i , j ] = D [ i , j − 1 ] + 1 D[i, j]=D[i, j-1] + 1 D[i,j]=D[i,j−1]+1。
替换是把 s s s串的最后一位替换为 t t t串的最后一位,如下图所示,但如果 s [ i ] = t [ j ] s[i]=t[j] s[i]=t[j],就不需要替换,则 D [ i , j ] = D [ i − 1 , j − 1 ] D[i, j]=D[i-1, j-1] D[i,j]=D[i−1,j−1];否则,需要替换,而且我们进一步需要关心的是如何从 s [ 1.. i − 1 ] s[1..i-1] s[1..i−1]变成 t [ 1.. j − 1 ] t[1..j-1] t[1..j−1],因此 D [ i , j ] = D [ i − 1 , j − 1 ] + 1 D[i, j]=D[i-1, j-1]+1 D[i,j]=D[i−1,j−1]+1。
3.2.2 构造递推公式
综合上面三种方式,我们可以得到递推公式
D
[
i
,
j
]
=
m
i
n
{
D
[
i
−
1
,
j
]
+
1
,
D
[
i
,
j
−
1
]
+
1
,
D
[
i
−
1
,
j
−
1
]
+
{
0
,
i
f
s
[
i
]
=
t
[
j
]
1
,
i
f
s
[
i
]
≠
t
[
j
]
D[i,j]=min\left\{ \begin{array}{rcl} D[i-1, j]+1,& &\\ D[i, j-1]+1,& &\\ D[i-1, j-1]+&\left\{ \begin{array}{rcl} 0, & &{if\ s[i]=t[j]}\\ 1, & &{if\ s[i] \neq t[j]} \end{array} \right. \end{array} \right.
D[i,j]=min⎩⎪⎪⎨⎪⎪⎧D[i−1,j]+1,D[i,j−1]+1,D[i−1,j−1]+{0,1,if s[i]=t[j]if s[i]=t[j]
3.3 自底向上计算
3.3.1 确定计算顺序
-
初始化:
- D [ i , 0 ] = i D[i, 0]=i D[i,0]=i(把长度为 i i i的串变为空串至少需要 i i i次删除操作)
- D [ 0 , j ] = j D[0, j]=j D[0,j]=j(把空串变为长度为 j j j的串至少需要 j j j次插入操作)
-
递推公式:
D [ i , j ] = m i n { D [ i − 1 , j ] + 1 , D [ i , j − 1 ] + 1 , D [ i − 1 , j − 1 ] + { 0 , i f s [ i ] = t [ j ] 1 , i f s [ i ] ≠ t [ j ] D[i,j]=min\left\{ \begin{array}{rcl} D[i-1, j]+1,& &\\ D[i, j-1]+1,& &\\ D[i-1, j-1]+&\left\{ \begin{array}{rcl} 0, & &{if\ s[i]=t[j]}\\ 1, & &{if\ s[i] \neq t[j]} \end{array} \right. \end{array} \right. D[i,j]=min⎩⎪⎪⎨⎪⎪⎧D[i−1,j]+1,D[i,j−1]+1,D[i−1,j−1]+{0,1,if s[i]=t[j]if s[i]=t[j]
3.3.2 依次计算问题
3.4 最优方案追踪
3.4.1 记录决策过程
用追踪数组 R e c Rec Rec来记录子问题来源。
3.4.2 输出最优方案
根据数组 R e c Rec Rec,输出最少编辑操作。
4. 伪代码
Minimum-Edit-Distance(s, t)
输入:字符串 s s s和 t t t
输出: s s s和 t t t的最小编辑距离
n ← length(s)
m ← length(t)
新建D[0..n, 0..m],Rec[0..n,0..m]两个二维数组
// 初始化
for i ← 0 to n do
D[i, 0] ← i
Rec[i, 0] ← "U"
end
for j ← 0 to m do
D[0, j] ← j
Rec[0, j] ← "L"
end
// 动态规划
for i ← 1 to n do
for j ← 1 to m do
c ← 0
if s[i] != t[j] then
c ← 1
end
replace ← D[i-1, j-1] + c
delete ← D[i-1, j] + 1
insert ← D[i, j-1] + 1
if replace = min{delete, insert, replace} then
D[i, j] ← D[i-1, j-1] + c
Rec[i, j] ← "LU"
end
else if insert = min{delete, insert, replace} then
D[i, j] ← D[i, j-1] + 1
Rec[i, j] ← "L"
end
else
D[i, j] ← D[i-1, j] + 1
Rec[i, j] ← "U"
end
end
end
Print-MED(Rec, s, t, i, j)
输入:矩阵Rec,字符串s,t,位置索引i和j
输出:操作序列
if i = 0 and j = 0 then
return NULL
end
if ReC[i, j] = "LU" then
Print-MED(Rec, s, t, i-1, j-1)
if s[i] = t[j] then
print "无需操作"
end
else
print "用t[j]替换s[i]"
end
end
else if Rec[i, j] = "U" then
Print-MED(Rec, s, t, i-1, j)
print "删除s[i]"
end
else
Print-MED(Rec, s, t, i, j-1)
print "插入t[j]"
end
该算法的时间复杂度是 O ( m n ) O(mn) O(mn)。