Leetcode 多维动态规划 1

题号直达: 62 不同路径 | 64 最小路径和 | 5 最长回文子串 | 1143 最长公共子序列 | 72 编辑距离

LeetCode 62. 不同路径 ↑

题目

一个机器人位于 m × n 网格的左上角(起点),每次只能向下或向右移动一格。求它到达右下角(终点)的不同路径条数。

测试样例

  • 示例 1: m = 3,n = 7
    输出:28
  • 示例 2: m = 3,n = 2
    输出:3
    解释:从左上到右下共 3 条不同路径。

提示

  • 1 <= m, n <= 100
  • 题目数据保证答案在 32 位有符号整数范围内

代码(Python)方法 1:组合数学

从起点到终点共走 (m - 1) + (n - 1) = m + n - 2 步,其中恰有 m - 1 步向下、n - 1 步向右,顺序不同即路径不同。方案数等于在 m + n - 2 个位置里选 m - 1 个放「向下」的组合数:

C(m + n − 2, m − 1)(也等于 C(m + n − 2, n − 1))。

实现上先算分子 (m + n − 2) × (m + n − 3) × … × n(共 m − 1 个因子),再除以 (m − 1)!,与上式等价。Python 3 下应用整数除法 // 得到整型答案。

class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        divide = 1
        ans = 1
        for i in range(1, m):
            divide = divide * i
        for j in range(0, m - 1):
            ans = ans * (m + n - 2 - j)

        return ans // divide

思路解析(方法 1)

等价计数:全程定长 m + n − 2 步,只需决定哪些步「向下」,其余自动为「向右」,故路径数 = 组合数 C(m + n − 2, m − 1)。

实现:分子为连续 m − 1 个整数相乘,分母为 (m − 1)!;循环结构与题中一致,最后 // 避免浮点。

  • 时间复杂度:O(m)。
  • 空间复杂度:O(1)。

代码(Python)方法 2:多维动态规划

f[i][j] 表示从左上角走到第 i 行、第 j 列(0 起下标)的路径条数。第一行、第一列只能沿单方向过来,故全为 1;其余格子由「从上进入」与「从左进入」相加。

class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        f = [[1] * n] + [[1] + [0] * (n - 1) for _ in range(m - 1)]
        for i in range(1, m):
            for j in range(1, n):
                f[i][j] = f[i - 1][j] + f[i][j - 1]
        return f[m - 1][n - 1]

思路解析(方法 2)

状态:f[i][j] = 到达 (i, j) 的路径数。
边界:第 0 行、第 0 列均为 1。
转移:只能下或右,故 f[i][j] = f[i - 1][j] + f[i][j - 1]。

初始化用一行全 1 与 m - 1 行「行首为 1、后面先置 0 再在循环里填」的写法,与双重循环转移一致。

  • 时间复杂度:O(m × n)。
  • 空间复杂度:O(m × n)(表格;可压成一维 O(n))。

LeetCode 64. 最小路径和 ↑

题目

给定一个包含非负整数的 m × n 网格 grid,找出一条从左上角到右下角的路径,使路径上经过的数字总和最小。

移动规则:每一步只能向下或向右移动一格。

最小路径和:题意与网格示例

测试样例

  • 示例 1: grid = [[1,3,1],[1,5,1],[4,2,1]]
    输出:7
    解释:路径 1 → 3 → 1 → 1 → 1 的和最小,为 7。
  • 示例 2: grid = [[1,2,3],[4,5,6]]
    输出:12

提示

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200

代码(Python)

class Solution(object):
    def minPathSum(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        dp = []
        for i in range(len(grid)):
            row = [0] * len(grid[0])
            dp.append(row)

        # 把边缘确定
        dp[0][0] = grid[0][0]
        for i in range(1, len(grid)):
            dp[i][0] = dp[i - 1][0] + grid[i][0]
        for j in range(1, len(grid[0])):
            dp[0][j] = dp[0][j - 1] + grid[0][j]

        for i in range(1, len(grid)):
            for j in range(1, len(grid[0])):
                dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1])

        return dp[-1][-1]

思路解析

状态:dp[i][j] = 从左上角到 (i, j) 的最小路径和。
边界:dp[0][0] = grid[0][0];第一列只能从上往下累加,第一行只能从左往右累加。
转移:其它格子由上或左走来,取较小累加当前格:dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1])。

答案为 dp[-1][-1](即 dp[m - 1][n - 1])。

  • 时间复杂度:O(m × n)。
  • 空间复杂度:O(m × n)(可改成原地修改 grid 或一维滚动为 O(n))。

LeetCode 5. 最长回文子串 ↑

题目

给定字符串 s,返回 s 中最长的回文子串。

回文串:正读与反读相同(如 "aba"、"bb")。

测试样例

  • 示例 1: s = "babad"
    输出:"bab"("aba" 亦为合法答案)。
  • 示例 2: s = "cbbd"
    输出:"bb"

提示

  • 1 <= s.length <= 1000
  • s 由数字和英文字母组成

代码(Python)

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        dp = []
        max_record = []
        for i in range(len(s)):
            temp = [False] * len(s)
            dp.append(temp)
        for i in range(len(s)):
            dp[i][i] = True
        for g in range(1, len(s)):
            for j in range(len(s) - g):  # j 为区间左端,区间 [j, j+g]
                if g == 1:
                    if s[j] == s[j + g]:
                        dp[j][j + g] = True
                        max_record = [j, j + g]
                else:
                    if dp[j + 1][j + g - 1] and s[j] == s[j + g]:
                        dp[j][j + g] = True
                        max_record = [j, j + g]
        if max_record:
            return s[max_record[0] : max_record[1] + 1]
        else:
            return s[0]

思路解析

最长回文子串:区间 DP 示意

状态:dp[i][j] 表示 s[i … j](闭区间)是否为回文串。边界:长度为 1 的子串 dp[i][i] = True。

按区间长度递增枚举:g 为「右端与左端的距离」,子串长度为 g + 1。

  • g == 1:长度为 2,只需 s[j] == s[j + g]。
  • g > 1:两端相等且内侧 s[j+1 … j+g-1] 为回文,即 dp[j + 1][j + g - 1] 且 s[j] == s[j + g]。

每次判定为回文时用 max_record 记录当前最长区间的左右端下标;若没有长度 ≥ 2 的回文(或未更新),则单独字符亦可,返回 s[0]。

  • 时间复杂度:O(n²),n = len(s)。
  • 空间复杂度:O(n²)(dp 表)。

LeetCode 1143. 最长公共子序列 ↑

题目

给定两个字符串 text1 和 text2,返回它们最长公共子序列的长度。若不存在公共子序列,返回 0。

子序列:由原串删除若干字符(可删 0 个)得到、且不改变剩余字符相对顺序的新串。
公共子序列:既是 text1 的子序列、也是 text2 的子序列。

测试样例

  • 示例 1: text1 = "abcde",text2 = "ace"
    输出:3
    解释:最长公共子序列可为 "ace"。
  • 示例 2: text1 = "abc",text2 = "abc"
    输出:3
  • 示例 3: text1 = "abc",text2 = "def"
    输出:0

提示

  • 1 <= text1.length, text2.length <= 1000
  • text1 与 text2 仅由小写英文字母组成

代码(Python)

class Solution(object):
    def longestCommonSubsequence(self, text1, text2):
        """
        :type text1: str
        :type text2: str
        :rtype: int
        """
        dp = []
        for i in range(len(text1) + 1):
            temp = [0] * (len(text2) + 1)
            dp.append(temp)

        for i in range(1, len(text1) + 1):
            for j in range(1, len(text2) + 1):
                if text1[i - 1] == text2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        return dp[-1][-1]

思路解析

最长公共子序列:DP 表与 pad 一圈 0 示意

须在 DP 矩阵最外侧「pad 一圈 0」(多一行、多一列,尺寸为 (len(text1) + 1) × (len(text2) + 1)):
对应 text1 的前 0 个字符或 text2 的前 0 个字符时,公共子序列长度恒为 0。这样不用单独写越界分支,循环 i, j 从 1 起,用 text1[i-1]、text2[j-1] 与 dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1] 对齐。

状态:dp[i][j] = text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度。
转移:若 text1[i-1] == text2[j-1],则 dp[i][j] = dp[i-1][j-1] + 1;否则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。答案 dp[-1][-1]。

  • 时间复杂度:O(m × n),m = len(text1),n = len(text2)。
  • 空间复杂度:O(m × n)(可滚动一维优化为 O(min(m,n)))。

LeetCode 72. 编辑距离 ↑

题目

给定两个字符串 word1 和 word2,返回将 word1 转换成 word2 所需要使用的最少操作数。你可以进行三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

测试样例

  • 示例 1: word1 = "horse",word2 = "ros"
    输出:3
  • 示例 2: word1 = "intention",word2 = "execution"
    输出:5

提示

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

代码(Python)

class Solution(object):
    def minDistance(self, word1, word2):
        """
        :type word1: str
        :type word2: str
        :rtype: int
        """

        dp = []
        for i in range(len(word1) + 1):
            temp = [0] * (len(word2) + 1)
            dp.append(temp)

        for i in range(len(dp)):
            dp[i][0] = i
        for j in range(len(dp[0])):
            dp[0][j] = j

        for i in range(1, len(dp)):
            for j in range(1, len(dp[0])):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = 1 + min(
                        dp[i - 1][j],
                        dp[i][j - 1],
                        dp[i - 1][j - 1] - 1,
                    )
                else:
                    dp[i][j] = 1 + min(
                        dp[i - 1][j],
                        dp[i][j - 1],
                        dp[i - 1][j - 1],
                    )

        return dp[-1][-1]

思路解析

状态:dp[i][j] 表示 word1 前 i 个字符与 word2 前 j 个字符之间的最小编辑距离。边界:dp[i][0] = i(逐个删除)、dp[0][j] = j(逐个插入)。

转移:末位不同时,删、插、换对应 dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1] 三者取最小再加 1。末位相同时,用 1 + min(..., dp[i-1][j-1] - 1) 的写法,与常见 dp[i][j] = dp[i-1][j-1](本轮不必改末位)等价。

更系统的推导、示意图与变形写法见:

72. 编辑距离 — 力扣官方题解

  • 时间复杂度:O(m × n)。
  • 空间复杂度:O(m × n)。
posted @ 2026-10-12 00:35  Phier  阅读(2)  评论(0)    收藏  举报