LeetCode72 编辑距离

121 阅读3分钟

leetcode.cn/problems/ed…

image.png

解法一:动态规划

编辑距离问题就是给我们两个字符串 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
}

参考

labuladong.online/algo/dynami…