解法一:动态规划
编辑距离问题就是给我们两个字符串 s1 和 s2,只能用三种操作,让我们把 s1 变成 s2,求最少的操作数。需要明确的是,不管是把 s1 变成 s2 还是反过来,结果都是一样的.
解决两个字符串的动态规划问题,一般都是用两个指针 i, j 分别指向两个字符串的头部或尾部,然后尝试写状态转移方程。
比方说让 i, j 分别指向两个字符串的尾部,把 dp[i], dp[j] 定义为 s1[0..i], s2[0..j] 子串的编辑距离,那么 i, j 一步步往前移动(逐渐减小)的过程,就是问题规模(子串长度)逐步减小的过程。
对于每对字符 s1[i] 和 s2[j],可以有四种操作:
if s1[i] == s2[j]:
啥都不需要做(skip)
也就是说
s1[0..i] 和 s2[0..j] 的最小编辑距离等于 s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离
i, j 同时向前移动
else:
三选一:
插入(insert)
删除(delete)
替换(replace)
这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。
暴力解法如下
func minDistance(s1 string, s2 string) int {
m, n := len(s1), len(s2)
// i,j 初始化指向最后一个索引,往前移动
return dp(s1, m - 1, s2, n - 1)
}
// 定义:返回 s1[0..i] 和 s2[0..j] 的最小编辑距离
func dp(s1 string, i int, s2 string, j int) int {
// base case
// 如果 `i` 走完 `s1` 时 `j` 还没走完了 `s2`,那就只能用插入操作把 `s2` 剩下的字符全部插入 `s1`
if i == -1 {
return j + 1
}
if j == -1 {
return i + 1
}
if s1[i] == s2[j] {
// 啥都不做
return dp(s1, i - 1, s2, j - 1)
}
return min(
// 插入
// 直接在 s1[i] 后面插入一个和 s2[j] 一样的字符,那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比
// 别忘了操作数加一
dp(s1, i, s2, j - 1) + 1,
// 删除
// 直接把 s[i] 这个字符删掉,那么前移i,继续计算 s1[0..i-1] 和 s2[0..j] 的最小编辑距离
dp(s1, i - 1, s2, j) + 1,
// 替换
// 直接把 s1[i] 替换成 s2[j],这样它俩就匹配了,同时前移 i,j 继续对比
dp(s1, i - 1, s2, j - 1) + 1
)
}
带备忘录优化
func minDistance(s1 string, s2 string) int {
// 备忘录
memo := make([][]int, len(s1))
for i := range memo {
memo[i] = make([]int, len(s2))
for j := range memo[i] {
memo[i][j] = -1
}
}
return dp(s1, len(s1)-1, s2, len(s2)-1, memo)
}
func dp(s1 string, i int, s2 string, j int, memo [][]int) int {
if i == -1 {
return j + 1
}
if j == -1 {
return i + 1
}
// 查备忘录,避免重叠子问题
if memo[i][j] != -1 {
return memo[i][j]
}
// 状态转移,结果存入备忘录
if s1[i] == s2[j] {
memo[i][j] = dp(s1, i-1, s2, j-1, memo)
} else {
memo[i][j] = min(
dp(s1, i, s2, j-1, memo)+1,
dp(s1, i-1, s2, j, memo)+1,
dp(s1, i-1, s2, j-1, memo)+1,
)
}
return memo[i][j]
}
func min(a int, b int, c int) int {
if a < b {
if a < c {
return a
}
return c
}
if b < c {
return b
}
return c
}
递推写法
func minDistance(s1 string, s2 string) int {
m, n := len(s1), len(s2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// base case
for i := 1; i <= m; i++ {
dp[i][0] = i
}
for j := 1; j <= n; j++ {
dp[0][j] = j
}
// 自底向上求解
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if s1[i-1] == s2[j-1] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = min(
dp[i-1][j]+1,
dp[i][j-1]+1,
dp[i-1][j-1]+1,
)
}
}
}
// 储存着整个 s1 和 s2 的最小编辑距离
return dp[m][n]
}
func min(a, b, c int) int {
if a < b {
if a < c {
return a
}
return c
}
if b < c {
return b
}
return c
}