LeetCode 第 72 题:编辑距离(动态规划)

地址:https://leetcode-cn.com/problems/edit-distance

思路:

  • 这个问题只问最少方案数,很显然是「动态规划」问题的问法,因此首先考虑使用「动态规划」方法去做;
  • 「动态规划」告诉我们可以「自底向上」去考虑一个问题,先想这个问题最开始是什么情况,这个问题是两个字符串都为空字符的时候,然后逐个地,一个字符一个字符加上去,在加字符的过程中考虑「状态转移」;
  • 由于要考虑空字符,因此状态空间要多设置一行、多设置一列。

方法:动态规划

第 1 步:定义状态

状态:dp[i][j] 表示将 word1[0, i) 转换成为 word2[0, j) 的方案数。

说明:由于要考虑空字符,这里的下标 i 不包括 word[i],同理下标 j 不包括 word[j],从行数和列数多设置一行、一列也可以来理解这一点,也就是状态的下标 ij 和字符的下标 ij 有一个位置的偏差。

第 2 步:思考状态转移方程

状态转移方程通常是在做分类讨论,而分类讨论的过程,常常利用了这个问题的「最优子结构」。

情况 1:如果 word1[i] == word2[j] 成立,则将 word1[0, i) 转换成为 word2[0, j) 的方案数就等于 将 word1[0, i - 1) 转换成为 word2[0, j - 1) 的方案数,即:

dp[i + 1][j + 1] = dp[i][j]

情况 2:如果 word1[i] != word2[j] ,则将 word1[0, i) 转换成为 word2[0, j) 的方案数就等于下面 3 种情况的最少操作数(「最优子结构」):

1、考虑将修改 word1[i] 成为 word2[j]

此时 dp[i + 1][j + 1] = dp[i][j] + 1,这里的 1 代表了将 word1[i] 替换成为 word2[j] 这一步操作。

2、考虑将 word1[0, i] 的最后一个字符删除;

此时 word1[0, i - 1]word2[0, j] 的最少操作数 + 1 + 1 +1,就是这种方案数的最少操作数,即: dp[i + 1][j + 1] = dp[i][j + 1] + 1,这里的 1 代表了 word1[0, i] 的最后一个字符删除这一步操作。

3、将 word1[0, i] 的末尾添加一个字符使得 word[i + 1] == word2[j]

此时考虑方案的时候,由于 word[i + 1] == word2[j],状态转移就不应该考虑 word2[j],因此 word1[0, i]word2[0, j - 1] 的最少操作数 + 1 + 1 +1,就是这种方案数的最少操作数,即: dp[i + 1][j + 1] = dp[i + 1][j] + 1,这里的 1 代表了将 word1[0, i] 的末尾添加一个字符使得 word[i + 1] == word2[j]。(注意:可以考虑一下为什么得先讨论 word1[i] == word2[j] 的情况。)

在这 3 种操作取最小值。

dp[i + 1][j + 1] = min(dp[i][j], dp[i][j + 1], dp[i + 1][j]) + 1

第 3 步:初始化

  • 从一个字符串变成空字符串,非空字符串的长度就是编辑距离;
  • 以下代码其实就是在填表格的第 0 0 0 行、第 0 0 0 列。
for (int i = 0; i <= len1; i++) {
    dp[i][0] = i;
}

for (int j = 0; j <= len2; j++) {
    dp[0][j] = j;
}

第 4 步: 思考输出

输出:dp[len1][len2] 符合语义,即 word1[0, len) 转换成 word2[0,len2) 的最小操作数。

第 5 步: 思考输出状态压缩

我们看一下「状态转移方程」:

  • 如果末尾字符相等,就「抄」左上角单元格的值;
  • 如果末尾字符不相等,就从「正上方」、「左边」、「左上角」三个单元格的值中选出最小的 + 1。

因此,初看可以使用「滚动数组」,更极端一点,用 2 × 2 2 \times 2 2×2 表格就可以完成操作。
但是真正去做「状态压缩」的时候,由于初始化的原因,发现没有那么容易,在这里不做「状态压缩」。

Java 代码:

import java.util.Arrays;

public class Solution {

    public int minDistance(String word1, String word2) {
        // 由于 word1.charAt(i) 操作会去检查下标是否越界,因此
        // 在 Java 里,将字符串转换成字符数组是常见额操作

        char[] word1Array = word1.toCharArray();
        char[] word2Array = word2.toCharArray();

        int len1 = word1Array.length;
        int len2 = word2Array.length;

        // 多开一行一列是为了保存边界条件,即字符长度为 0 的情况,这一点在字符串的动态规划问题中比较常见
        int[][] dp = new int[len1 + 1][len2 + 1];

        // 初始化:当 word 2 长度为 0 时,将 word1 的全部删除即可
        for (int i = 1; i <= len1; i++) {
            dp[i][0] = i;
        }
        // 当 word1 长度为 0 时,就插入所有 word2 的字符即可
        for (int j = 1; j <= len2; j++) {
            dp[0][j] = j;
        }

        // 注意:填写 dp 数组的时候,由于初始化多设置了一行一列,横纵坐标有个偏移
        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                // 这是最佳情况
                if (word1Array[i] == word2Array[j]) {
                    dp[i + 1][j + 1] = dp[i][j];
                    continue;
                }

                // 否则在以下三种情况中选出步骤最少的,这是「动态规划」的「最优子结构」
                // 1、在下标 i 处插入一个字符
                int insert = dp[i + 1][j] + 1;
                // 2、替换一个字符
                int replace = dp[i][j] + 1;
                // 3、删除一个字符
                int delete = dp[i][j + 1] + 1;
                dp[i + 1][j + 1] = Math.min(Math.min(insert, replace), delete);

            }
        }

        // 打印状态表格进行调试
//        for (int i = 0; i <=len1; i++) {
//            System.out.println(Arrays.toString(dp[i]));
//        }
        return dp[len1][len2];
    }

    public static void main(String[] args) {
        String word1 = "horse";
        String word2 = "ros";

        Solution solution = new Solution();
        int res = solution.minDistance(word1, word2);
        System.out.println(res);
    }
}

建议:自己动手填表格,加深「动态规划」,作为一种表格法是如何填表的体会。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值