编辑距离

[传送门]( 72. 编辑距离 – 力扣(LeetCode) )

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

一、思路

编辑距离问题就是给我们两个字符串 s1s2,只能用三种操作,让我们把 s1 变成 s2,求最少的操作数。需要明确的是,不管是把 s1 变成 s2 还是反过来,结果都是一样的,所以后文就以 s1 变成 s2 举例。

解决两个字符串的动态规划问题,一般都是用两个指针 i, j 分别指向两个字符串的最后,然后一步步往前移动,缩小问题的规模

PS:其实让 i, j 从前往后移动也可以,改一下 dp 函数/数组的定义即可,思路是完全一样的。

base case 是 i 走完 s1j 走完 s2,可以直接返回另一个字符串剩下的长度。

对于每对儿字符 s1[i]s2[j],可以有四种操作:

if s1[i] == s2[j]:
    啥都别做(skip)
    i, j 同时向前移动
else:
    三选一:
        插入(insert)
        删除(delete)
        替换(replace)

二、暴力解法代码会超时😢

int minDistance(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // i,j 初始化指向最后一个索引
    return dp(s1, m - 1, s2, n - 1);
}

// 定义:返回 s1[0..i] 和 s2[0..j] 的最小编辑距离
int dp(String s1, int i, String s2, int j) {
    // base case
    if (i == -1) return j + 1;
    if (j == -1) return i + 1;

    if (s1.charAt(i) == s2.charAt(j)) {
        return dp(s1, i - 1, s2, j - 1); // 啥都不做
        # 解释:
        # 本来就相等,不需要任何操作
        # s1[0..i] 和 s2[0..j] 的最小编辑距离等于
        # s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离
        # 也就是说 dp(i, j) 等于 dp(i-1, j-1)
    }
    return min(
        dp(s1, i, s2, j - 1) + 1,    // 插入
        
        # 我直接在 s1[i] 插入一个和 s2[j] 一样的字符
        # 那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比
        # 别忘了操作数加一
        
        dp(s1, i - 1, s2, j) + 1,    // 删除
        
        # 我直接把 s[i] 这个字符删掉
        # 前移 i,继续跟 j 对比
        # 操作数加一
        
        dp(s1, i - 1, s2, j - 1) + 1 // 替换
        
        # 我直接把 s1[i] 替换成 s2[j],这样它俩就匹配了
        # 同时前移 i,j 继续对比
        # 操作数加一
        
    );
}

int min(int a, int b, int c) {
    return Math.min(a, Math.min(b, c));
}

还有点小问题就是,这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。

怎么能一眼看出存在重叠子问题呢

int dp(i, j) {
    dp(i - 1, j - 1); // #1
    dp(i, j - 1);     // #2
    dp(i - 1, j);     // #3
}

对于子问题 dp(i-1, j-1),如何通过原问题 dp(i, j) 得到呢?有不止一条路径,比如 dp(i, j) -> #1dp(i, j) -> #2 -> #3。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。

三、动态规划优化

对于重叠子问题呢,优化方法无非是备忘录或者 DP table

备忘录很好加,原来的代码稍加修改即可:

// 备忘录
int[][] memo;
    
public int minDistance(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 备忘录初始化为特殊值,代表还未计算
    memo = new int[m][n];
    for (int[] row : memo) {
        Arrays.fill(row, -1);
    }
    return dp(s1, m - 1, s2, n - 1);
}

int dp(String s1, int i, String s2, int j) {
    if (i == -1) return j + 1;
    if (j == -1) return i + 1;
    // 查备忘录,避免重叠子问题
    if (memo[i][j] != -1) {
        return memo[i][j];
    }
    // 状态转移,结果存入备忘录
    if (s1.charAt(i) == s2.charAt(j)) {
        memo[i][j] = dp(s1, i - 1, s2, j - 1);
    } else {
        memo[i][j] =  min(
            dp(s1, i, s2, j - 1) + 1,
            dp(s1, i - 1, s2, j) + 1,
            dp(s1, i - 1, s2, j - 1) + 1
        );
    }
    return memo[i][j];
}

int min(int a, int b, int c) {
    return Math.min(a, Math.min(b, c));
}

主要说下 DP table 的解法

首先明确 dp 数组的含义,dp 数组是一个二维数组,长这样:

1667796150393

有了之前递归解法的铺垫,应该很容易理解。dp[..][0]dp[0][..] 对应 base case,dp[i][j] 的含义和之前的 dp 函数类似:

int dp(String s1, int i, String s2, int j)
// 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离

dp[i-1][j-1]
// 存储 s1[0..i] 和 s2[0..j] 的最小编辑距离

dp 函数的 base case 是 i, j 等于 -1,而数组索引至少是 0,所以 dp 数组会偏移一位。

既然 dp 数组和递归 dp 函数含义一样,也就可以直接套用之前的思路写代码,唯一不同的是,DP table 是自底向上求解,递归解法是自顶向下求解

int minDistance(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    // 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp[i+1][j+1]
    int[][] dp = new int[m + 1][n + 1];
    // base case 
    for (int i = 1; i <= m; i++)
        dp[i][0] = i;
    for (int j = 1; j <= n; j++)
        dp[0][j] = j;
    // 自底向上求解
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s1.charAt(i-1) == s2.charAt(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];
}

int min(int a, int b, int c) {
    return Math.min(a, Math.min(b, c));
}

原文地址:http://www.cnblogs.com/ANDQE/p/16875314.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性