算法leetcode|72. 编辑距离(rust重拳出击)


文章目录


72. 编辑距离:

给你两个单词 word1word2, 请返回将 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')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成

分析:

  • 面对这道算法题目,二当家的再次陷入了沉思。

  • 编辑距离算法在实际应用中还是很多的,比如一些命令的参数,当输入了错误的参数时,会提示最相似的命令。
    - 想要找最优解,一般就是贪心或者动态规划。

  • 思考后会发现,完整串的编辑距离和子串的编辑距离有关系,所以考虑使用动态规划。

  • 别急,这里还有一个问题,题目中可以对两个单词分别进行三种操作,所以相当于一共有六种操作,其中插入字符依赖较短字符串,而删除字符的操作就反向依赖了较长串,但是动态规划是从一个初识条件开始,朝着一个方向计算的,这里依赖着两种方向,这怎么办?

  • 其实,我们可以将相同效果的操作合并处理:

    1. 对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;

    2. 同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;

    3. 对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。

  • 这样一来,本质不同的操作实际上只有三种:

    1. 在单词 A 中插入一个字符;

    2. 在单词 B 中插入一个字符;

    3. 修改单词 A 的一个字符。

  • 这样一来,我们就可以把原问题转化为规模较小的子问题。以样例1为例,我们用 A = horse,B = ros 作为例子,来看一看是如何把这个问题转化为规模较小的若干子问题的:

    1. 在单词 A 中插入一个字符:如果我们知道 horse 到 ro 的编辑距离为 a,那么显然 horse 到 ros 的编辑距离不会超过 a + 1。这是因为我们可以在 a 次操作后将 horse 和 ro 变为相同的字符串,只需要额外的 1 次操作,在单词 A 的末尾添加字符 s,就能在 a + 1 次操作后将 horse 和 ro 变为相同的字符串;

    2. 在单词 B 中插入一个字符:如果我们知道 hors 到 ros 的编辑距离为 b,那么显然 horse 到 ros 的编辑距离不会超过 b + 1,原因同上;

    3. 修改单词 A 的一个字符:如果我们知道 hors 到 ro 的编辑距离为 c,那么显然 horse 到 ros 的编辑距离不会超过 c + 1,原因同上。

  • 那么从 horse 变成 ros 的编辑距离应该为 min(a + 1, b + 1, c + 1)。

  • 因此,我们就可以使用动态规划来解决这个问题了。我们用 Dij 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。

  • 如上所述,当我们获得 Dij-1,Di-1j 和 Di-1j-1 的值之后就可以计算出 Dij

    1. Dij-1 为 A 的前 i 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们在 A 的末尾添加了一个相同的字符,那么 Dij 最小可以为 Dij-1 + 1;

    2. Di-1j 为 A 的前 i - 1 个字符和 B 的前 j 个字符编辑距离的子问题。即对于 A 的第 i 个字符,我们在 B 的末尾添加了一个相同的字符,那么 Dij 最小可以为 Di-1j + 1;

    3. Di-1j-1 为 A 前 i - 1 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们修改 A 的第 i 个字符使它们相同,那么 Dij 最小可以为 Di-1j-1 + 1。特别地,如果 A 的第 i 个字符和 B 的第 j 个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,Dij 最小可以为 Di-1j-1

  • 一般题解到这里就结束了,但其实我们还可以继续优化空间。

  • 由于动态规划中,我们比较两个子串,只依赖于各减少最后一个字符的子串的编辑距离,所以我们的动态规划数组是可以重复利用的,不需要二维数组,只需要一维数组即可,即滚动数组的方式。


题解:

rust:

二维数组(易懂)

rust 复制代码
impl Solution {
    pub fn min_distance(word1: String, word2: String) -> i32 {
        let l1 = word1.len();
        let l2 = word2.len();

        // 有一个字符串为空串
        if l1 == 0 || l2 == 0 {
            return (l1 + l2) as i32;
        }

        // DP 数组
        let mut dp = vec![vec![0; l2 + 1]; l1 + 1];

        // 边界状态初始化
        (0..=l1).for_each(|i| {
            dp[i][0] = i;
        });
        (0..=l2).for_each(|i| {
            dp[0][i] = i;
        });

        // 计算所有 DP 值
        (1..=l1).for_each(|i| {
            (1..=l2).for_each(|j| {
                let insert1 = dp[i - 1][j] + 1;
                let insert2 = dp[i][j - 1] + 1;
                let replace1 = if word1.as_bytes()[i - 1] != word2.as_bytes()[j - 1] {
                    dp[i - 1][j - 1] + 1
                } else {
                    // 两个字母相同,不用修改,所以操作次数不变
                    dp[i - 1][j - 1]
                };
                dp[i][j] = insert1.min(insert2).min(replace1);
            });
        });

        return dp[l1][l2] as i32;
    }
}

滚动数组(更加优化的内存空间)

rust 复制代码
impl Solution {
    pub fn min_distance(mut word1: String, mut word2: String) -> i32 {
        let mut l1 = word1.len();
        let mut l2 = word2.len();

        // 有一个字符串为空串
        if l1 == 0 {
            return l2 as i32;
        }
        if l2 == 0 {
            return l1 as i32;
        }

        // 让内层单词较短,可以让dp数组较小
        if l1 < l2 {
            let wt = word1;
            word1 = word2;
            word2 = wt;

            let lt = l1;
            l1 = l2;
            l2 = lt;
        }

        // DP 滚动数组
        let mut dp = (0..=l2).collect::<Vec<_>>();

        // 计算所有 DP 值
        word1.bytes().enumerate().for_each(|(i1, c1)| {
            let mut pre = i1;
            dp[0] = pre + 1;

            word2.bytes().enumerate().for_each(|(i2, c2)| {
                let tmp = dp[i2 + 1];

                if c1 == c2 {
                    dp[i2 + 1] = pre;
                } else {
                    // dp[i2 + 1]:相当于向第一个单词插入一个字母
                    // dp[i2]:相当于向第二个单词插入一个字母
                    // pre: 相当于修改第一个单词一个字母

                    dp[i2 + 1] = dp[i2 + 1].min(dp[i2]).min(pre) + 1;
                }

                pre = tmp;
            });
        });

        dp[l2] as i32
    }
}

go:

go 复制代码
func minDistance(word1 string, word2 string) int {
    l1 := len(word1)
	l2 := len(word2)

	// 有一个字符串为空串
	if l1 == 0 {
		return l2
	}
	if l2 == 0 {
		return l1
	}

	// 让内层单词较短,可以让dp数组较小
	if l1 < l2 {
		word1, word2 = word2, word1
		l1, l2 = l2, l1
	}

	// DP 滚动数组
	dp := make([]int, l2+1)
	for i := 1; i <= l2; i++ {
		dp[i] = i
	}

	// 计算所有 DP 值
	for i1, c1 := range word1 {
		pre := i1
		dp[0] = pre + 1

		for i2, c2 := range word2 {
			tmp := dp[i2+1]

			if c1 == c2 {
				dp[i2+1] = pre
			} else {
				// dp[i2 + 1]:相当于向第一个单词插入一个字母
				// dp[i2]:相当于向第二个单词插入一个字母
				// pre: 相当于修改第一个单词一个字母

				if dp[i2+1] > dp[i2] {
					dp[i2+1] = dp[i2]
				}
				if dp[i2+1] > pre {
					dp[i2+1] = pre
				}
				dp[i2+1] += 1
			}

			pre = tmp
		}
	}

	return dp[l2]
}

c++:

cpp 复制代码
class Solution {
public:
    int minDistance(string word1, string word2) {
        int l1 = word1.length(), l2 = word2.length();

        // 有一个字符串为空串
        if (l1 == 0) {
            return l2;
        }
        if (l2 == 0) {
            return l1;
        }

        // 让内层单词较短,可以让dp数组较小
        if (l1 < l2) {
            string wt = word1;
            word1 = word2;
            word2 = wt;

            int lt = l1;
            l1 = l2;
            l2 = lt;
        }

        // DP 滚动数组
        int dp[l2 + 1];
        for (int i = 1; i <= l2; ++i) {
            dp[i] = i;
        }

        // 计算所有 DP 值
        for (int i1 = 0; i1 < l1; ++i1) {
            int pre = i1;
            dp[0] = pre + 1;

            for (int i2 = 0; i2 < l2; ++i2) {
                const int tmp = dp[i2 + 1];

                if (word1[i1] == word2[i2]) {
                    dp[i2 + 1] = pre;
                } else {
                    // dp[i2 + 1]:相当于向第一个单词插入一个字母
                    // dp[i2]:相当于向第二个单词插入一个字母
                    // pre: 相当于修改第一个单词一个字母
                    dp[i2 + 1] = min(min(dp[i2 + 1], dp[i2]), pre) + 1;
                }

                pre = tmp;
            }
        }

        return dp[l2];
    }
};

python:

python 复制代码
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        l1 = len(word1)
        l2 = len(word2)

        # 有一个字符串为空串
        if l1 == 0:
            return l2
        if l2 == 0:
            return l1

        # 让内层单词较短,可以让dp数组较小
        if l1 < l2:
            word1, word2 = word2, word1
            l1, l2 = l2, l1

        # DP 数组
        dp = [x for x in range(l2 + 1)]

        # 计算所有 DP 值
        for i1 in range(l1):
            pre = i1
            dp[0] = pre + 1

            for i2 in range(l2):
                tmp = dp[i2 + 1]

                if word1[i1] == word2[i2]:
                    dp[i2 + 1] = pre
                else:
                    # dp[i2 + 1]:相当于向第一个单词插入一个字母
                    # dp[i2]:相当于向第二个单词插入一个字母
                    # pre: 相当于修改第一个单词一个字母
                    dp[i2 + 1] = min(dp[i2 + 1], dp[i2], pre) + 1

                pre = tmp

        return dp[l2]

java:

java 复制代码
class Solution {
    public int minDistance(String word1, String word2) {
        int l1 = word1.length(), l2 = word2.length();

        // 有一个字符串为空串
        if (l1 == 0) {
            return l2;
        }
        if (l2 == 0) {
            return l1;
        }

        // 让内层单词较短,可以让dp数组较小
        if (l1 < l2) {
            String wt = word1;
            word1 = word2;
            word2 = wt;

            int lt = l1;
            l1 = l2;
            l2 = lt;
        }

        // DP 滚动数组
        int[] dp = new int[l2 + 1];
        for (int i = 1; i <= l2; ++i) {
            dp[i] = i;
        }

        // 计算所有 DP 值
        for (int i1 = 0; i1 < l1; ++i1) {
            int pre = i1;
            dp[0] = pre + 1;

            for (int i2 = 0; i2 < l2; ++i2) {
                final int tmp = dp[i2 + 1];

                if (word1.charAt(i1) == word2.charAt(i2)) {
                    dp[i2 + 1] = pre;
                } else {
                    // dp[i2 + 1]:相当于向第一个单词插入一个字母
                    // dp[i2]:相当于向第二个单词插入一个字母
                    // pre: 相当于修改第一个单词一个字母
                    dp[i2 + 1] = Math.min(Math.min(dp[i2 + 1], dp[i2]), pre) + 1;
                }

                pre = tmp;
            }
        }

        return dp[l2];
    }
}

非常感谢你阅读本文~

欢迎【点赞】【收藏】【评论】三连走一波~

放弃不难,但坚持一定很酷~

希望我们大家都能每天进步一点点~

本文由 二当家的白帽子:https://le-yi.blog.csdn.net/ 博客原创~


相关推荐
明月_清风11 分钟前
用自然语言控制 Blender:开源项目 Blender MCP 深度介绍
人工智能·后端
weixin_3077791317 分钟前
C++代码实现MATLAB中的ode45函数功能
开发语言·c++·算法·matlab
张小姐的猫24 分钟前
【AI大模型接入SDK】 —— 数据管理 & 与Session模块进行联动
数据结构·数据库·c++·人工智能·python·chatgpt
不穿鞋的懒羊羊41 分钟前
dfs深度优先搜索
算法
OYYHXPJR1 小时前
【算法】区间重叠模板和矩形重叠模板
算法
shehuiyuelaiyuehao1 小时前
算法46,分治快排,排序数组
java·算法·排序算法·排序
JaguarJack1 小时前
Whim 是什么?一门把 PHP 被否决的设想真跑起来的实验语言
后端·php·服务端
圣保罗的大教堂1 小时前
leetcode 3483. 不同三位偶数的数目 简单
leetcode
careathers1 小时前
【数据结构】栈
java·数据结构
明月_清风1 小时前
MHS:AI Agent 开始连接物理世界
人工智能·后端·网络协议