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.lengthn == grid[i].length1 <= m, n <= 2000 <= 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 <= 1000s由数字和英文字母组成
代码(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[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 <= 1000text1与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」(多一行、多一列,尺寸为 (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:
word1 = "horse",word2 = "ros"
输出:3 - 示例 2:
word1 = "intention",word2 = "execution"
输出:5
提示
0 <= word1.length, word2.length <= 500word1和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](本轮不必改末位)等价。
更系统的推导、示意图与变形写法见:
- 时间复杂度:O(m × n)。
- 空间复杂度:O(m × n)。

浙公网安备 33010602011771号