题目描述:
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入: word1 = "intention", word2 = "execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
解题思路:
什么是动态规划?通俗地理解来说,一个问题的解决办法一看就知道(穷举),但不能一个一个数啊,你得找到最优的解决办法,换句话说题目中就会出现类似“最多”、“最少”,“一共有多少种”等提法,这些题理论上都能使用动态规划的思想来求解。动态规划与分治方法类似,都是通过组合子问题的解来求解原问题,但它对每个子问题只求解一次,将其保存在表格中,无需重新计算,通常用于求解最优化问题——《算法导论》。
编辑距离(Edit Distance),在本文指的是Levenshtein
距离,也就是字符串S1
通过插入、修改、删除三种操作最少能变换成字符串S2
的次数。例如:S1 = abc,S2 = abf,
编辑距离d = 1
(只需将c修改为f)。在本文中将利用动态规划的算法思想对字符串的编辑距离求解。
定义:S1、S2表示两个字符串,S1(i)
表示 S1 的第
i
i
i 个字符,d[i, j]
表示S1
的第
i
i
i 个前缀到 S2
的第j
个前缀(例如:S1 = ”abc”,S2 = ”def”,
求解S1到S2的编辑距离为d[3, 3]
)。
-
若
S1 = ”abc”, S2 = ”dec”
,此时它们的编辑距离为d[3, 3] = 2
,观察两个字符串的最后一个字符是相同的,也就是说S1(3) = S2(3)
不需要做任何变换,故S1 = ”abc”, S2 = ”dec” <= > S1’ = ”ab”, S2’ = ”de”
,即当S1[i] = S[j]
时,d[i, j] = d[i-1,j -1]
。得到公式:d[i, j] = d[i - 1, j - 1] (S1[i] = S2[j])
-
上面一条得出了当
S1[i] = S2[j]
的计算公式,显然还有另一种情况就是S1[i] ≠ S2[j]
,若S1 = ”abc”, S2 = ”def”
。S1变换到S2的过程可以“修改”,但还可以通过“插入”、“删除”使得S1
变换为S2
。
-
在S1字符串末位插入字符 “f”,此时
S1 = ”abcf”,S2 = ”def”
,此时即S1[i] = S2[j]
的情况,S1变换为S2的编辑距离为d[4, 3] = d[3, 2]
。所以得出d[i, j]=d[i, j - 1] + 1
。(+1是因为S1新增了”f”) -
在S2字符串末位插入字符“c”,此时
S1 = ”abc”,S2 = ”defc”
,此时即S1[i] = S[j]
的情况,S1
变换为S2的编辑距离为d[3, 4] = d[2, 3]
。所以得出d[i, j]=d[i - 1, j] + 1
,实际上这是对S1
做了删除。(+1是因为S2新增了”c”) -
将S1字符串末位字符修改为
”f”
,此时S1 = ”abf”,S2 = ”def”
,此时即S1[i] = S[j]
的情况,S1变换为S2的编辑距离为d[3, 3] = d[2, 2]
。所以得出d[i, j] = d[i – 1, j - 1] + 1
。(+1是因为S1修改了“c”)
不妨用表格表示出动态规划对S1=”abc”,S2=“def”的求解过程
可以看出红色方块即是最终所求的编辑距离,整个求解过程就是填满这个表——二维数组。
代码1:
'''
动态规划——字符串的编辑距离
s1 = "abc", s2 = "def"
计算公式:
| 0 i = 0, j = 0
| j i = 0, j > 0
d[i,j] = | i i > 0, j = 0
| min(d[i,j-1]+1, d[i-1,j]+1, d[i-1,j-1]) s1(i) = s2(j)
| min(d[i,j-1]+1, d[i-1,j]+1, d[i-1,j-1]+1) s1(i) ≠ s2(j)
定义二维数组[4][4]:
d e f d e f
|x|x|x|x| |0|1|2|3|
a |x|x|x|x| => a |1|1|2|3| => 编辑距离d = [4][4] = 3
b |x|x|x|x| b |2|2|2|3|
c |x|x|x|x| c |3|3|3|3|
'''
def levenshtein(s1, s2):
i = 0 # s1字符串中的字符下标
j = 0 # s2字符串中的字符下标
s1i = "" # s1字符串第i个字符
s2j = "" # s2字符串第j个字符
m = len(s1) # s1字符串长度
n = len(s2) # s2字符串长度
if m == 0:
return n # s1字符串长度为0,此时的编辑距离就是s2字符串长度
if n == 0:
return m # s2字符串长度为0,此时的编辑距离就是s1字符串长度
solutionMatrix = [[0 for col in range(n + 1)] for row in range(m + 1)] # 长为m+1,宽为n+1的矩阵
'''
d e f
|0|x|x|x|
a |1|x|x|x|
b |2|x|x|x|
c |3|x|x|x|
'''
for i in range(m + 1):
solutionMatrix[i][0] = i
'''
d e f
|0|1|2|3|
a |x|x|x|x|
b |x|x|x|x|
c |x|x|x|x|
'''
for j in range(n + 1):
solutionMatrix[0][j] = j
'''
上面两个操作后,求解矩阵变为
d e f
|0|1|2|3|
a |1|x|x|x|
b |2|x|x|x|
c |3|x|x|x|
接下来就是填充剩余表格
'''
for x in range(1, m + 1):
s1i = s1[x - 1]
for y in range(1, n + 1):
s2j = s2[y - 1]
flag = 0 if s1i == s2j else 1
solutionMatrix[x][y] = min(solutionMatrix[x][y-1] + 1, solutionMatrix[x-1][y] + 1, solutionMatrix[x-1][y-1] + flag)
return solutionMatrix[m][n]
def min(insert, delete, edit):
tmp = insert if insert < delete else delete
return tmp if tmp < edit else edit
s1 = "abc"
s2 = "def"
distance = levenshtein(s1, s2)
print(distance)
代码2:
class Solution(object):
def minDistance(self, word1, word2):
m = len(word1)
n = len(word2)
if m == 0:
return n
if n == 0:
return m
dp = [[0]*(n+1) for i in range(m+1)]
dp[0][0] = 0
for i in range(1,m+1):
dp[i][0] = i
for j in range(1,n+1):
dp[0][j] = j
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1+min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) # 方格的左上,上,左最小值+1
return dp[m][n]
s1 = "abc"
s2 = "def"
s = Solution()
distance = s.minDistance(s1, s2)
print(distance)
C++写法:
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> f(n+1, vector<int>(m+1));
// 初始化边界情况
for(int i = 0; i <= n; i++)
f[i][0] = i; // 第一个字符串的前i个字母变为第二个字符串的前0个(删除)
for(int i = 0; i <= m; i++)
f[0][i] = i; // 第一个字符串的前0个字母变为第二个字符串的前i个(添加)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++){
f[i][j] = min(f[i][j-1]+1, f[i-1][j]+1); // 添加和删除的情况
f[i][j] = min(f[i][j], f[i-1][j-1]+ (word1[i-1] != word2[j-1]));
}
return f[n][m];
}
};
参考链接:
动态规划(1)——字符串的编辑距离
leetcode–72–编辑距离