1. 题目解析与核心思路
LeetCode 1594题要求我们在一个m×n的矩阵中找到从左上角到右下角的路径,使得路径上所有数字的乘积最大且非负。这道题看似简单,但实际考察了动态规划、状态转移和空间优化等多个算法核心概念。
首先我们需要明确几个关键点:
- 路径只能向右或向下移动
- 乘积需要考虑正负号的影响
- 最终结果必须是非负数
- 需要处理大数情况(题目提示结果可能很大,需要对10^9+7取模)
1.1 为什么常规DP会失效
传统的动态规划解法(只记录最大乘积)在这里会失效,原因在于负数的存在。考虑这个简单例子:
code复制[1, 2]
[3, -4]
如果只记录最大值,我们会选择1→3→-4路径,得到-12。但实际上最优解是1→2→-4,得到-8的绝对值更大(题目要求非负,所以实际结果是8)。
1.2 双状态DP的必要性
因此我们需要同时记录每个位置的最大值和最小值。这是因为:
- 正数×负数会变成更小的负数
- 负数×负数会变成更大的正数
- 我们需要同时跟踪这两种可能性
状态转移方程为:
code复制maxDP[i][j] = max(
grid[i][j] * maxDP[i-1][j],
grid[i][j] * maxDP[i][j-1],
grid[i][j] * minDP[i-1][j],
grid[i][j] * minDP[i][j-1]
)
minDP[i][j] = min(
// 同上,取最小值
)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 记忆化搜索实现
2.1 递归+记忆化方案
我们可以先用递归思路实现,再添加记忆化优化:
python复制MOD = 10**9 + 7
def maxProductPath(grid):
m, n = len(grid), len(grid[0])
memo = {}
def dfs(i, j, current):
if (i, j, current) in memo:
return memo[(i, j, current)]
if i == m-1 and j == n-1:
return current * grid[i][j]
res = -float('inf')
if i + 1 < m:
res = max(res, dfs(i+1, j, current * grid[i][j]))
if j + 1 < n:
res = max(res, dfs(i+1, j, current * grid[i][j]))
memo[(i, j, current)] = res
return res
result = dfs(0, 0, 1)
return result % MOD if result >= 0 else -1
注意:这个基础版本存在两个问题:1) 没有处理最小值状态 2) 参数current会导致状态爆炸
2.2 改进的记忆化搜索
更合理的记忆化搜索应该同时跟踪最大和最小值:
python复制def maxProductPath(grid):
m, n = len(grid), len(grid[0])
memo = {}
def dfs(i, j):
if i == m-1 and j == n-1:
return (grid[i][j], grid[i][j])
if (i, j) in memo:
return memo[(i, j)]
max_val = -float('inf')
min_val = float('inf')
if i + 1 < m:
down_max, down_min = dfs(i+1, j)
candidates = [grid[i][j] * down_max, grid[i][j] * down_min]
max_val = max(max_val, max(candidates))
min_val = min(min_val, min(candidates))
if j + 1 < n:
right_max, right_min = dfs(i, j+1)
candidates = [grid[i][j] * right_max, grid[i][j] * right_min]
max_val = max(max_val, max(candidates))
min_val = min(min_val, min(candidates))
memo[(i, j)] = (max_val, min_val)
return max_val, min_val
max_res, min_res = dfs(0, 0)
return max(max_res, min_res) % MOD if max(max_res, min_res) >= 0 else -1
3. 递推式动态规划实现
3.1 基础DP实现
我们可以将记忆化搜索转换为递推形式:
python复制def maxProductPath(grid):
m, n = len(grid), len(grid[0])
max_dp = [[0]*n for _ in range(m)]
min_dp = [[0]*n for _ in range(m)]
max_dp[0][0] = min_dp[0][0] = grid[0][0]
# 初始化第一列
for i in range(1, m):
val = grid[i][0]
max_dp[i][0] = max_dp[i-1][0] * val
min_dp[i][0] = min_dp[i-1][0] * val
# 初始化第一行
for j in range(1, n):
val = grid[0][j]
max_dp[0][j] = max_dp[0][j-1] * val
min_dp[0][j] = min_dp[0][j-1] * val
# 填充剩余部分
for i in range(1, m):
for j in range(1, n):
val = grid[i][j]
candidates = [
max_dp[i-1][j] * val,
min_dp[i-1][j] * val,
max_dp[i][j-1] * val,
min_dp[i][j-1] * val
]
max_dp[i][j] = max(candidates)
min_dp[i][j] = min(candidates)
result = max_dp[-1][-1]
return result % MOD if result >= 0 else -1
3.2 边界条件处理
有几个关键边界需要注意:
- 当grid[0][0]为0时,需要特殊处理
- 当中间过程出现0时,最大最小值都会变成0
- 最终结果必须非负
4. 空间优化技巧
4.1 滚动数组优化
观察状态转移方程,我们发现每次只需要前一行的数据,因此可以将空间复杂度从O(mn)降到O(n):
python复制def maxProductPath(grid):
m, n = len(grid), len(grid[0])
prev_max = [0] * n
prev_min = [0] * n
prev_max[0] = prev_min[0] = grid[0][0]
# 初始化第一行
for j in range(1, n):
val = grid[0][j]
prev_max[j] = prev_max[j-1] * val
prev_min[j] = prev_min[j-1] * val
for i in range(1, m):
curr_max = [0] * n
curr_min = [0] * n
curr_max[0] = curr_min[0] = prev_max[0] * grid[i][0]
for j in range(1, n):
val = grid[i][j]
candidates = [
prev_max[j] * val,
prev_min[j] * val,
curr_max[j-1] * val,
curr_min[j-1] * val
]
curr_max[j] = max(candidates)
curr_min[j] = min(candidates)
prev_max, prev_min = curr_max, curr_min
result = prev_max[-1]
return result % MOD if result >= 0 else -1
4.2 进一步的空间优化
我们甚至可以只用两个变量来存储前一列的值,将空间降到O(1):
python复制def maxProductPath(grid):
m, n = len(grid), len(grid[0])
# 处理第一行
prev_max = prev_min = grid[0][0]
for j in range(1, n):
val = grid[0][j]
prev_max, prev_min = prev_max * val, prev_min * val
for i in range(1, m):
curr_max = curr_min = prev_max * grid[i][0]
for j in range(1, n):
val = grid[i][j]
candidates = [
prev_max * val,
prev_min * val,
curr_max * val,
curr_min * val
]
new_max = max(candidates)
new_min = min(candidates)
prev_max, prev_min = curr_max, curr_min
curr_max, curr_min = new_max, new_min
prev_max, prev_min = curr_max, curr_min
result = prev_max
return result % MOD if result >= 0 else -1
注意:这种极致的空间优化虽然节省了内存,但会牺牲一些代码可读性,在实际面试中建议先实现标准版本,再讨论优化可能。
5. 常见问题与调试技巧
5.1 典型错误案例
- 只记录最大值:如前面所述,这会错过负数相乘变正数的情况
- 初始化错误:忘记初始化第一行和第一列
- 模运算时机错误:应该在最后才取模,中间过程取模会影响大小比较
- 整数溢出:Python不需要担心,但其他语言需要注意
5.2 调试建议
- 先用小矩阵测试(2x2或3x3)
- 打印中间状态矩阵
- 特别注意包含0和负数的case
5.3 测试用例设计
好的测试用例应该包含:
python复制tests = [
# 常规正数
([[1,2,3],[4,5,6]], 360),
# 包含负数
([[1,-2,3],[4,-5,6]], 360),
# 包含0
([[1,0,3],[4,5,6]], 120),
# 全负数
([[-1,-2],[-3,-4]], 8),
# 单个元素
([[5]], 5),
# 结果必须非负
([[-1,-1,-1],[-1,-1,-1]], 1)
]
6. 算法复杂度分析
- 时间复杂度:O(mn),因为需要遍历整个矩阵
- 空间复杂度:
- 基础DP:O(mn)
- 滚动数组:O(n)
- 极致优化:O(1)
在实际应用中,对于较大的矩阵(如1000x1000),空间优化能显著减少内存使用。
7. 实际应用场景
这种双状态DP技巧可以应用于:
- 游戏中的路径得分最大化(考虑增益和减益)
- 金融领域的最优投资路径计算
- 图像处理中的像素值累积运算
- 任何需要考虑正负影响的序列决策问题
8. 扩展思考
- 如果允许四个方向移动(形成环)会怎样?
- 问题会变得复杂,可能需要图算法
- 如果要求绝对值最大(可以是负数)?
- 只需比较最终的最大和最小值的绝对值
- 如果矩阵非常大无法放入内存?
- 需要考虑分块处理或流式算法
这道题很好地展示了动态规划的灵活性,也提醒我们在解决最优化问题时,不能简单套用模板,而要充分理解问题特性,设计合适的状态表示。
