网格上的DP——从左上到右下,每一步都在做最优决策
网格路径DP是最直观的DP模型:在m*n的网格中从左上角走到右下角,每步只能向右或向下。核心状态定义:dp[i][j] = 到达位置(i,j)的某个属性(最小和、路径数等)。
def grid_dp(grid):
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = grid[0][0]
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j] # 只能从左来
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0] # 只能从上来
for i in range(1, m):
for j in range(1, n):
dp[i][j] = f(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
给定m*n的非负整数网格,找到从左上到右下的路径,使路径上数字之和最小。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = grid[0][0]
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
# 原地修改版本 O(1)空间
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
for j in range(1, n):
grid[0][j] += grid[0][j-1]
for i in range(1, m):
grid[i][0] += grid[i-1][0]
for i in range(1, m):
for j in range(1, n):
grid[i][j] += min(grid[i-1][j], grid[i][j-1])
return grid[m-1][n-1]
时间: O(m * n) 空间: O(m * n),原地修改O(1)
一个m*n的网格,从左上到右下有多少条不同路径?
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
# 组合数学版本
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
from math import comb
return comb(m + n - 2, m - 1)
时间: O(m * n) 空间: O(m * n),可优化到O(n)
网格中有障碍物,求从左上到右下的不同路径数。
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1 if obstacleGrid[0][0] == 0 else 0
for j in range(1, n):
dp[0][j] = dp[0][j-1] if obstacleGrid[0][j] == 0 else 0
for i in range(1, m):
dp[i][0] = dp[i-1][0] if obstacleGrid[i][0] == 0 else 0
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 0:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
时间: O(m * n) 空间: O(m * n)
在实际面试中,算法题不仅仅是写代码,更考察沟通、分析和解决问题的能力。以下是一些实战技巧:
# 通用解题模板
class Solution:
def solve(self, input_data):
# 1. 边界检查
if not input_data:
return default_value
# 2. 初始化
result = initial_value
state = initial_state
# 3. 主循环
for item in input_data:
state = transition(state, item)
result = max/min/update(result, state)
# 4. 返回结果
return result
每道经典题目都有丰富的变体。掌握核心模板后,灵活应对变体才是面试的真正挑战。以下列举本课相关的高频变体和扩展思路。
# 面试沟通模板
# 1. 先说思路(1-2分钟)
"这道题我想到两种解法,先说更直观的一种..."
# 2. 分析复杂度(30秒)
"时间复杂度O(n^2),空间可以优化到O(n)"
# 3. 边写边说
"这里我定义dp[i]表示...,转移方程是..."
# 4. 主动验证
"我用示例走一遍:dp[0]=..., dp[1]=..."
# 5. 讨论优化
"空间还可以进一步优化,因为dp[i]只依赖..."
| 检查项 | 常见错误 |
|---|---|
| 初始条件 | dp[0]是否正确?空输入是否处理? |
| 遍历范围 | 循环从0还是1开始?是否越界? |
| 状态转移 | 依赖的状态是否已计算? |
| 返回值 | 是dp[n]还是max(dp)? |
| 边界情况 | 单元素、全相同、空数组 |
网格路径DP的变形非常丰富,以下是最常见的变体。
| 变体 | 题目 | DP定义 | 特殊处理 |
|---|---|---|---|
| 三角形网格 | LC 120 | dp[i][j]=到(i,j)最小和 | 每行长度不同 |
| 3方向下降 | LC 931 | dp[i][j]=到(i,j)最小和 | 从左上/正上/右上来 |
| 反向DP | LC 174 | dp[i][j]=从(i,j)到终点最少血量 | 从右下往左上 |
| 障碍网格 | LC 63 | dp[i][j]=路径数 | 障碍位=0 |
| 必须经过所有空格 | LC 980 | bitmask+位置 | 状态压缩 |