LeetCode热题100多维动态规划题解析

178 阅读6分钟

难度标识:⭐:简单,⭐⭐:中等,⭐⭐⭐:困难

tips:这里的难度不是根据LeetCode难度定义的,而是根据我解题之后体验到题目的复杂度定义的。

1.不同路径

思路

解这题依然是使用动态规划的方法,核心思路如下:

定义一个二维数组 dp,其中 dp[i][j] 表示从起始点到点 (i, j) 的路径数量。

初始化条件:

  1. 当 i = 0 或 j = 0 时,dp[i][j] = 1,因为机器人只能一直向下或一直向右走。

状态转移方程:

  1. 对于其他的 i 和 j,机器人可以从上方 (i-1, j) 或左方 (i, j-1) 到达 (i, j)。所以 dp[i][j] = dp[i-1][j] + dp[i][j-1]。

最终结果:

  1. dp[m-1][n-1] 就是我们要的答案,也就是从起点到终点的路径数。

代码

var uniquePaths = function (m, n) {
    const dp = new Array(m + 1).fill(1).map(() => new Array(n + 1).fill(1))
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        }
    }
    return dp[m - 1][n - 1]
};

2.最小路径和

思路

核心思路:

  1. 状态定义:我们定义一个与原始网格同样大小的二维数组 dp,其中 dp[i][j] 表示从左上角到网格的 (i, j) 位置的最小路径和。

  2. 初始化边界:由于从左上角开始,我们只能向右或向下移动,所以第一行的每个位置的最小路径和是前一个位置的值加上当前位置的值。同样,第一列的每个位置的最小路径和是上一个位置的值加上当前位置的值。

  3. 状态转移方程:对于 (i, j) 位置,有两种方式到达此位置:从 (i-1, j) 向下移动或从 (i, j-1) 向右移动。所以 dp[i][j] 的值是从这两个位置中选择一个较小值,然后加上当前位置的值。

具体的状态转移方程为:dp[i][j]=grid[i][j]+min(dp[i−1][j],dp[i][j−1])

  1. 结果:最后,dp[m-1][n-1] 就是从左上角到右下角的最小路径和。下面的代码我优化了一下,就用原始的数组的每个位置来放左上角到每个位置的最小路径和。

代码

var minPathSum = function (grid) {
    const m = grid.length, n = grid[0].length
    for (let i = 1; i < m; i++) {
        grid[i][0] += grid[i - 1][0]
    }
    for (let j = 1; j < n; j++) {
        grid[0][j] += grid[0][j - 1]
    }
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1])
        }
    }
    return grid[m - 1][n - 1]
};

3.最长回文子串 ⭐⭐

思路

这题就不使用动态规划了,前面刷左右双指针的时候我刷过这题了,所以这题还是使用双指针的方法解,只是特殊一点,叫中心扩展法,这个方法解起来还是挺简单的。

  1. 回文特性:回文是关于中心对称的。换句话说,回文的一半可以决定另一半。

  2. 中心的数量:在一个长度为 n 的字符串中,可能的回文中心有 2n-1 个。这可能听起来有点奇怪,但考虑到回文可以是奇数长度(例如,“aba”有中心'b')或偶数长度(例如,“abba”有中心在两个'b'之间),我们可以认为每两个字符之间和每个字符都可能是回文的中心。

代码

var longestPalindrome = function (s) {
    let res = ''
    for (let i = 0; i < s.length; i++) {
        const oddStr = expandCenter(s, i, i)
        const evenStr = expandCenter(s, i, i + 1)
        res = res.length > oddStr.length ? res : oddStr
        res = res.length > evenStr.length ? res : evenStr
    }
    return res
};
function expandCenter(s, left, right) {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
        left--
        right++
    }
    return s.substring(left + 1, right)
}

4.最长公共子序列

思路

这题有个很好的视频,解释的非常清楚,一看就懂,建议观看视频学习。

核心思路是构建一个二维数组(通常称为dp),其中dp[i][j]代表text1的前i个字符和text2的前j个字符的最长公共子序列(LCS)的长度。

核心思路如下:

  1. 初始化:构建一个(len(text1) + 1) x (len(text2) + 1)的二维数组,全部初始化为0。其中,dp[0][j]dp[i][0]都是0,因为空字符串与任何字符串的LCS长度都是0。

  2. 状态转移

    • 如果text1[i-1] == text2[j-1],这意味着当前字符是LCS的一部分,所以dp[i][j] = dp[i-1][j-1] + 1
    • 否则,LCS可以从text1的前i-1个字符和text2的前j个字符获得,或者从text1的前i个字符和text2的前j-1个字符获得。所以,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  3. 结果dp[len(text1)][len(text2)]会给出text1text2的LCS的长度。

代码

var longestCommonSubsequence = function (text1, text2) {
    const m = text1.length, n = text2.length
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0))
    for (let i = 1; i < m + 1; i++) {
        for (let j = 1; j < n + 1; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
            }
        }
    }
    return dp[m][n]
};

5.编辑距离

思路

这题跟上面那题一样,也有个很好的视频,解释的非常清楚,一看就懂,建议观看视频学习。

我们可以定义一个二维数组dp,其中dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需的最小操作数。

核心思路如下:

  1. 初始化dp[0][j]表示将空字符串转换为word2的前j个字符,它需要的操作数是j(插入j次)。同样,dp[i][0]表示将word1的前i个字符转换为空字符串,它需要的操作数是i(删除i次)。

  2. 状态转移

    • 如果word1[i-1] == word2[j-1],那么dp[i][j] = dp[i-1][j-1],因为当前的字符已经匹配,无需额外操作。

    • 否则:

      • 插入操作:将word1的前i个字符转换为word2的前j-1个字符,然后插入word2[j-1]。这需要dp[i][j-1] + 1操作。

      • 删除操作:将word1的前i-1个字符转换为word2的前j个字符,然后删除word1[i-1]。这需要dp[i-1][j] + 1操作。

      • 替换操作:将word1的前i-1个字符转换为word2的前j-1个字符,然后替换word1[i-1]word2[j-1]。这需要dp[i-1][j-1] + 1操作。

      • 最小操作数为上述三者中的最小值。

  3. 结果dp[len(word1)][len(word2)]给出将word1转换为word2所需的最小操作数。

上面的核心思路看不懂的,建议看视频,几分钟的视频立马让你茅塞顿开。

代码

var minDistance = function (word1, word2) {
    const m = word1.length, n = word2.length
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0))
    for (let i = 0; i < m + 1; i++) {
        dp[i][0] = i
    }
    for (let j = 0; j < n + 1; j++) {
        dp[0][j] = j
    }
    for (let i = 1; i < m + 1; i++) {
        for (let j = 1; j < n + 1; j++) {
            if (word1[i - 1] === word2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1]
            } else {
                dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1])
            }
        }
    }
    return dp[m][n]
};

总结,多维动态规划问题相对于前面的动态规划类问题,感觉更加的简单,不知道是不是我看视频的原因,视频讲的确实一下就懂了,所以后面2题建议看视频理解,超级容易理解和简单。