最小编辑距离

117 阅读2分钟

leetcode 72 编辑距离 这是一道比较经典的动态规划算法题。

简单记录下该题的思路和代码。该题是要求算出最小编辑距离,也就是从字符串1到字符串2,所需要的步数

在这里也会做一点扩展,给出从字符串1到字符串2的编辑操作路径的解法。

第一部分 求最小值

思路如下:

  • 用两个指针i, j分别指向word1,word2两个字符串的最后,一步步往前,缩小问题规模。
  • base case: 当i走完word1或者j走完word2,则可返回另一个字符串剩下的长度。
  • 操作类型:对于字符串比对的操作有四种,跳过 删除 替换 插入。对比的字符相同则跳过,不相同则执行其他三种操作。
  • dp定义:返回word1[0...i]和word2[0...j]的最小编辑距离。

思路参考详解(labuladong)

先来看看最直接的暴力解法:

var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length
    // 初始值指向最后一个索引
    return dp(m-1, n-1)
    function dp(i, j) {
        if (i===-1) return j+1
        if (j===-1) return i+1
        if (word1[i]===word2[j]) {
            return dp(i-1, j-1)
        } else {
            // 别忘了在每步操作后加1
            return Math.min(
                dp(i-1, j) + 1, // 删除
                dp(i, j-1) + 1, // 插入
                dp(i-1, j-1) + 1 // 替换
            )
        }
    }
};

当然,暴力解法存在重叠子问题,可能会超过时间限制。

  1. 利用备忘录优化下:
var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length
    // fill不能直接传入数组,会指向同一个引用
    let memo = new Array(m).fill().map(() => new Array(n).fill(-1))
    return dp(m - 1, n - 1)
    function dp(i, j) {
        if (i === -1) return j + 1
        if (j === -1) return i + 1
        if (memo[i][j] !== -1) return memo[i][j]
        if (word1[i] === word2[j]) {
            memo[i][j] = dp(i - 1, j - 1)
        } else {
            memo[i][j] = Math.min(
                dp(i - 1, j) + 1,
                dp(i, j - 1) + 1,
                dp(i - 1, j - 1) + 1
            )
        }
        return memo[i][j]
    }
};
  1. 如果利用dp数组优化,则是这样:
var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length
    // 考虑到一方可能为空字符串的情况,数组长度分别为m+1、n+1
    let dp = new Array(m+1).fill().map(() => new Array(n+1).fill())
    for (let i = 0;i <= m; i++) {
        dp[i][0] = i
    }
    for (let j = 0; j <= n;j++) {
        dp[0][j] = j
    }
    for (let i = 1; i <= m; i++) {
         for (let j = 1; j <= n;j++) {
             // 注意此处的i-1、j-1, 确保数组索引是从0开始。
             if (word1[i-1] === word2[j-1]) {
                 dp[i][j] = dp[i-1][j-1]
             } else {
                 dp[i][j] = Math.min(
                     dp[i-1][j] +1,
                     dp[i][j-1] + 1,
                     dp[i-1][j-1] + 1
                 )
             }
         }
    }
    return dp[m][n]
};

第二部分 具体操作路径

第一部分已给出求最小编辑距离的解法,那如果想知道word1是如何一步步变成word2,也就是如何得到具体的操作路径呢?

可以给dp数组增加额外的信息,dp数组除了保存之前的距离值外,可以多保存一个操作值,即在计算最短距离的同时把操作值记录下来,这样就可以从最终的结果dp[m][n]反向推导出具体的操作路径。

如下,声明一个节点,记录距离值和操作值。

class Node {
  constructor(val, choice) {
    this.val = val
    this.choice = choice
  }
  val = -1
  choice // 记录操作 0-跳过 1-删除 2-插入 3-替换
}

完整代码如下:

代码中getPath(dp, m, n)所得的值即为所求操作路径。

class Node {
  constructor(val, choice) {
    this.val = val
    this.choice = choice
  }
  val = -1
  choice // 记录操作 0-跳过 1-删除 2-插入 3-替换
}

var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length
    // 考虑到一方可能为空字符串的情况,数组长度分别为m+1、n+1
    let dp = new Array(m+1).fill().map(() => new Array(n+1).fill().map(()=> new Node()))
    for (let i = 0;i <= m; i++) {
         // 若j指针走完,i没走完,则剩下的操作就是删除掉word1剩下的字符即可
        dp[i][0] = new Node(i, 1)
    }
    for (let j = 0; j <= n;j++) {
        // 若i指针走完,j没走完,则剩下的操作就是插入word2剩下的字符即可
        dp[0][j] = new Node(j, 2)
    }

    for (let i = 1; i <= m; i++) {
         for (let j = 1; j <= n;j++) {
             // 注意此处的i-1、j-1, 确保数组索引是从0开始。
             if (word1[i-1] === word2[j-1]) {
                 dp[i][j].val = dp[i-1][j-1].val
                 dp[i][j].choice = 0 // 跳过
             } else {
                 const [min, index] = findMin(
                     dp[i-1][j],
                     dp[i][j-1],
                     dp[i-1][j-1]
                 )
                 dp[i][j].val = min.val + 1
                 dp[i][j].choice = [1, 2, 3][index]
             }
         }
    }
    console.log(getPath(dp, m, n))
    return dp[m][n].val
};

function findMin(a, b, c) {
    let min = [a, b, c].sort((a, b) => {
        return a.val - b.val
    })[0]
    let index = [a, b, c].findIndex(i => i.val === min.val)
    return [min, index]
}

// 获取操作路径
function getPath(dp, m, n) {
    let position = [m, n]
    let endPoint = dp[m][n]
    const textMap = ['跳过', '删除', '插入', '替换']
    const res = []
    while (endPoint.val) {
        if (endPoint.choice === undefined) break
        res.unshift(textMap[endPoint.choice])
        const [i, j] = getPrev(endPoint.choice, position)
        position = [i, j]
        endPoint = dp[i][j]
    }
    return res
}

// 获取上一个节点的位置
function getPrev(choice, [m, n]) {
    return posMap = [        [m-1, n-1],
        [m-1, n],
        [m, n-1],
        [m-1, n-1]
    ][choice]
}