🧮 Algorithm Notebook
1、算法准备
1.1 牛客网编程OJ的典型输入输出
2、算法框架
2.1 动态规划
2.1.1 斐波那契数列
2.1.2 背包问题
2.1.3 贪心算法
2.1.4 序列和数组类问题
2.1.5 编辑距离
2.1.6 高楼扔鸡蛋
2.1.7 股票问题系列通解
2.1.8 最长有效括号
2.1.9 剪绳子
2.1.10 正则表达式匹配
2.2 二分查找
2.2.1 二分查找框架
2.2.2 搜索旋转排序数组
2.3 链表
2.3.1 反转链表
2.3.2 相交链表
2.3.3 合并链表
2.3.4 链表中倒数第k个节点
2.3.5 复制带随机指针的链表
2.4 排序算法
2.4.1 常见排序算法
2.5 二叉树
2.5.1 二叉树遍历
2.5.2 岛屿问题
2.5.3 二叉树路径问题
2.5.4 构造二叉树
2.6 回溯算法
2.6.1 回溯算法解题框架
2.6.2 N皇后
2.7 数组
2.7.1 删除有序数组中的重复项
2.7.2 滑动窗口最大值
2.7.3 调整数组顺序使奇数位于偶数前面
2.7.4 螺旋矩阵
2.7.5 多数元素
2.7.6 最大数
2.7.7 和为s的两个数字
2.7.8 构建乘积数组
2.7.9 两数之和
2.8 字符串
2.8.1 最小覆盖子串
2.8.2 比较版本号
2.8.3 验证IP地址
2.8.4 基本计算器 II
2.8.5 字符串解码
2.8.6 移掉 K 位数字
2.8.7 无重复字符的最长子串
2.8.8 第一个只出现一次的字符
2.8.9 翻转字符串里的单词
2.8.10 字符串转换整数 (atoi)
2.8.11 字符串四则运算
2.9 栈
2.9.1 最小栈
2.9.2 弹出序列
2.10 数学
2.10.1 用 Rand7() 实现 Rand10()
2.10.2 只出现一次的数字
2.10.3 整数反转
2.10.4 求1+2+…+n
2.10.5 二进制中1的个数
2.10.6 幂运算
2.10.7 1~n 整数中 1 出现的次数
2.10.8 数字序列中某一位的数字
2.10.9 丑数
2.10.10 n个骰子的点数
2.10.11 圆圈中最后剩下的数字
2.10.12 不用加减乘除做加法
2.10.13 x 的平方根
2.11 设计
2.11.1 LRU 缓存机制
-
+
tourist
register
Sign in
编辑距离
## 1 题目 给你两个单词 word1 和 word2,请你计算出将 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 * word1 和 word2 由小写英文字母组成 ## 2 解题思路 ### 2.1 递归 #### 2.1.1 问题解析 **解决两个字符串的动态规划问题,一般都是用两个指针 `i、j` 分别指向两个字符串的最后,然后一步一步往前走,缩小问题的规模。** 设两个字符串分别为 `rad` 和 `apple`,为了把 `s1` 变成 `s2`,算法会这样进行: ![](/media/202104/2021-04-06_204148.png) 根据上面的 GIF,可以发现操作不止有三个,其实还有第四个操作,就是什么都不要做(skip),比如这个情况: ![](/media/202104/2021-04-06_204612.png) 因为这两个字符本来就相同,为了使编辑距离最小,显然不应该对他们有任何操作,直接往前移动 `i、j` 即可。 还有一个很容易处理的情况,就是 `j` 走完 `s2` 时,如果 `i` 还没走完 `s1`,那么只能用删除操作把 `s1` 缩短为 `s2`,比如这个情况: ![](/media/202104/2021-04-06_204858.png) 类似的,如果 `i` 走完 `s1` 时 `j` 还没走完 `s2`,那就只能用插入操作把 `s2` 剩下的字符全部插入 `s1`,这两种情况就是算法的**base case**。 #### 2.1.2 代码解析 先梳理下之前的思路: **base case**是 `i` 走完 `s1` 或 `j` 走完 `s2`,可以直接返回另一个字符串剩下的长度。 对于每队字符 `s1[i]` 和 `s2[j]`,可以有四种操作: ```c++ if s1[i] == s2[j]: 啥都别做(skip) i, j 同时向前移动 else: 三选一: 插入(insert) 删除(delete) 替换(replace) ``` 对于这三个操作,需要全试一遍,哪个操作最后得到的编辑距离小,就选谁,具体代码如下: ```c++ def minDistance(s1, s2) -> int: def dp(i, j): # base case if i == -1: return j + 1 if j == -1: return i + 1 if s1[i] == s2[j]: return dp(i - 1, j - 1) # 啥都不做 else: return min( dp(i, j - 1) + 1, # 插入 dp(i - 1, j) + 1, # 删除 dp(i - 1, j - 1) + 1 # 替换 ) # i,j 初始化指向最后一个索引 return dp(len(s1) - 1, len(s2) - 1) ``` 下面对这段递归代码进行以下解释。 1. **dp[i][j]表示将 `s1[i]` 之前的字符修改为 `s2[j]` 之前的字符的最小编辑距离。** ```c++ def dp(i, j) -> int ``` 2. **如果 `s1[i]==s2[j]`:** 1. **说明这两个字符本来就相等,不需要进行任何操作。** 2. **此时 `s1[0..i]` 和 `s2[0..j]` 的最小编辑距离等于 `s1[0..i-1]` 和 `s2[0..j-1]` 的最小编辑距离。** 3. **`dp(i,j)` 等于 `dp(i-1,j-1)`。** ```c++ if s1[i] == s2[j]: return dp(i - 1, j - 1) ``` 3. **如果 `s1[i]!=s2[j]`,此时就需要对三个操作递归了:** 1. **插入:** **直接在 `s1[i]` 插入一个和 `s2[j]` 相同的字符,那么 `s2[j]` 就被匹配了,前移 `j`,继续跟 `i` 对比,同时操作数加 1。** ```c++ dp(i, j - 1) + 1, # 插入 ``` ![](/media/202104/2021-04-06_210959.png) 2. **删除:** **直接把 `s[i]` 这个字符删掉,前移 `i`,继续跟 `j` 对比,同时操作数加 1。** ```c++ dp(i - 1, j) + 1, # 删除 ``` ![](https://gblobscdn.gitbook.com/assets%2F-MWvhB2heCSJoT6IpxDY%2Fsync%2F36559b37dd118d77713ebc57ecfdcf11a2de599a.gif?alt=media) 3. **替换:直接把 `s1[i]` 替换成 `s2[j]`,这样他俩就匹配了,然后前移 `i、j`,并将其继续对比,同时操作数加 1。** ```c++ dp(i - 1, j - 1) + 1 # 替换 ``` ![](https://gblobscdn.gitbook.com/assets%2F-MWvhB2heCSJoT6IpxDY%2Fsync%2Fb2153166d681c0557bb40d2276f9b5707ba9b252.gif?alt=media) #### 2.1.3 参考代码 ```java /** * 返回三个数之间的最小值 * @param a 第一个参数 * @param b 第二个参数 * @param c 第三个参数 * @return 三个数之间的最小值 */ int min(int a, int b, int c) { return Math.min(a, Math.min(b, c)); } /** * dp 函数(版本 1) * @param s1 第一个字符串 * @param s2 第二个字符串 * @param i s1 的最后一个字符下标 * @param j s2 的最后一个字符下标 * @return s1 前 i 个字符替换成 s2 前 j 个字符所需要的最小编辑次数 */ int dpV1(String s1, String s2, int i, int j) { // base case // 1. 如果第一个单词遍历到最左边,则把 s2 剩余测字符全插入到 s1 前面 if (i == -1) {return j + 1;} // 2. 如果第二个单词遍历到最左边,则把 s1 剩余字符都删除 if (j == -1) {return i + 1;} // 如果两个字符串当前字符相同,则啥也不做 if (s1.charAt(i) == s2.charAt(j)) {return dpV1(s1, s2, i - 1, j - 1);} else { // 否则,一次尝试插入、删除、替换三种操作,并返回编辑距离最小的距离 return min( // 插入 dpV1(s1, s2, i, j - 1) + 1, // 删除 dpV1(s1, s2, i - 1, j) + 1, dpV1(s1, s2, i - 1, j - 1) + 1 ); } } /** * 72. 编辑距离(版本 1:动态规划) * 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 * 你可以对一个单词进行如下三种操作: * 插入一个字符 * 删除一个字符 * 替换一个字符 * @param word1 第一个单词 * @param word2 第二个单词 * @return 最少操作数 */ public int minDistanceV1(String word1, String word2) { return dpV1(word1, word2, word1.length() - 1, word2.length() - 1); } ``` ### 2.2 动态规划优化 动态规划的优化方法主要有两种,一种是**备忘录**,另一种是**DP Table**。 #### 2.2.1 备忘录优化 ##### 2.2.1.1 问题解析 备忘录优化主要是把上面递归方法中的 `dp(i,j)` 的数据保存在**备忘录**(实质是一个二维数组)中,每次递归时先判断 `dp(i,j)` 的数据有没有在备忘录中,如果有的话直接返回,没有的话再进行计算即可。 ##### 2.2.1.2 参考代码 ```java /** * 返回三个数之间的最小值 * @param a 第一个参数 * @param b 第二个参数 * @param c 第三个参数 * @return 三个数之间的最小值 */ int min(int a, int b, int c) { return Math.min(a, Math.min(b, c)); } /** * dp 函数(版本 2:备忘录优化) * @param s1 第一个字符串 * @param s2 第二个字符串 * @param i s1 的最后一个字符下标 * @param j s2 的最后一个字符下标 * @return s1 前 i 个字符替换成 s2 前 j 个字符所需要的最小编辑次数 */ int dpV2(String s1, String s2, int i, int j, int[][] memo) { // base case // 1. 如果第一个单词遍历到最左边,则把 s2 剩余测字符全插入到 s1 前面 if (i == -1) {return j + 1;} // 2. 如果第二个单词遍历到最左边,则把 s1 剩余字符都删除 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] = dpV2(s1, s2, i - 1, j - 1, memo);} else { // 否则,一次尝试插入、删除、替换三种操作,并返回编辑距离最小的距离 memo[i][j] = min( // 插入 dpV2(s1, s2, i, j - 1, memo) + 1, // 删除 dpV2(s1, s2, i - 1, j, memo) + 1, dpV2(s1, s2, i - 1, j - 1, memo) + 1 ); } // 返回结果 return memo[i][j]; } /** * 72. 编辑距离(版本 2:动态规划(备忘录优化)) * 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 * 你可以对一个单词进行如下三种操作: * 插入一个字符 * 删除一个字符 * 替换一个字符 * @param word1 第一个单词 * @param word2 第二个单词 * @return 最少操作数 */ public int minDistanceV2(String word1, String word2) { // 定义备忘录,并将其里面的每个元素初始化为 -1 int[][] memo = new int[word1.length()][word2.length()]; for (int i = 0; i < memo.length; i++) { Arrays.fill(memo[i], -1); } // 递归获取最小编辑距离 return dpV2(word1, word2, word1.length() - 1, word2.length() - 1, memo); } ``` #### 2.2.2 DP Table 优化 ##### 2.2.2.1 问题解析 1. **定义 `dp[i][j]`:** 1. `dp[i][j]` 代表`word1` 中前`i` 个字符,变换到`word2` 中前`j` 个字符,最短需要操作的次数。 2. 需要考虑`word1` 或`word2` 一个字母都没有,即**全增加**或**全删除**的情况,所以预留`dp[0][j]` 和`dp[i][0]`。 2. **状态转移:** 1. **增:**`dp[i][j] = dp[i][j-1] + 1` 2. **删:**`dp[i][j] = dp[i-1][j] + 1` 3. **改:**`dp[i][j] = dp[i-1][j-1]` 4. 按顺序计算,当计算`dp[i][j]` 时,`dp[i][j-1]、dp[i-1][j]、dp[i-1][j-1]` 均已经确定。 5. 配合**增删改**这三种操作,需要对应的`dp` 把操作数加 1,取三种的最小。 6. 如果刚好这两个字母相同,即`word1[i-1]=word2[j-1]`,那么可以直接参考`dp[i-1][j-1]`,操作不用加 1。 具体的图解如下: * **绿色:** 增。 * **红色:** 删。 * **黄色:** 改。 ![](/media/202104/2021-04-06_220218.png) ![](/media/202104/2021-04-06_220327.png) ![](/media/202104/2021-04-06_220334.png) ![](/media/202104/2021-04-06_220341.png) ![](/media/202104/2021-04-06_220348.png) ![](/media/202104/2021-04-06_220356.png) ##### 2.2.2.2 参考代码 ```java /** * 返回三个数之间的最小值 * @param a 第一个参数 * @param b 第二个参数 * @param c 第三个参数 * @return 三个数之间的最小值 */ int min(int a, int b, int c) { return Math.min(a, Math.min(b, c)); } /** * 72. 编辑距离(版本 3:动态规划(DP Table 优化)) * 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 * 你可以对一个单词进行如下三种操作: * 插入一个字符 * 删除一个字符 * 替换一个字符 * @param word1 第一个单词 * @param word2 第二个单词 * @return 最少操作数 */ public int minDistanceV3(String word1, String word2) { int m = word1.length(), n = word2.length(); 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 (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];} else { dp[i][j] = min( dp[i][j - 1] + 1, dp[i - 1][j] + 1, dp[i - 1][j - 1] + 1 ); } } } // 储存着整个 word1 和 word2 的最小编辑距离 return dp[m][n]; } ``` ## 3 参考文献 1. [72. 编辑距离](https://leetcode-cn.com/problems/edit-distance)。 2. [经典动态规划:编辑距离](https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/zi-xu-lie-lei-xing-wen-ti/bian-ji-ju-li)。 3. [【编辑距离】入门动态规划,你定义的 dp 里到底存了啥](https://leetcode-cn.com/problems/edit-distance/solution/edit-distance-by-ikaruga)。
ricear
Sept. 6, 2021, 8:36 p.m.
©
BY-NC-ND(4.0)
转发文档
Collection documents
Last
Next
手机扫码
Copy link
手机扫一扫转发分享
Copy link
Markdown文件
share
link
type
password
Update password