1. 项目概述:构造乘积矩阵的算法挑战
今天要拆解的是力扣第17题"构造乘积矩阵",这是一道考察数组操作和数学思维的中等难度题目。我在第一次遇到这个问题时,花了整整两小时才理清思路,后来发现其实有非常优雅的解法。这道题的核心要求是:给定一个矩阵,构造一个新矩阵,其中每个元素等于原矩阵所有元素乘积除以该位置元素的值(不能使用除法运算)。
举个例子,输入矩阵:
code复制[
[1, 2, 3],
[4, 5, 6]
]
期望输出:
[
[720, 360, 240],
[180, 144, 120]
]
因为720=2×3×4×5×6,360=1×3×4×5×6,以此类推。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解法思路
2.1 暴力解法及其缺陷
最直观的想法是双重循环遍历矩阵,对每个元素计算其他所有元素的乘积。这种方法时间复杂度高达O(n²m²),对于100×100的矩阵就需要1亿次运算,显然不实用。
python复制# 暴力解法示例(仅作演示,实际不可行)
def product_matrix_brute(matrix):
m, n = len(matrix), len(matrix[0])
result = [[1]*n for _ in range(m)]
for i in range(m):
for j in range(n):
for x in range(m):
for y in range(n):
if x != i or y != j:
result[i][j] *= matrix[x][y]
return result
2.2 优化思路:前缀积与后缀积
更聪明的做法是借鉴一维数组乘积问题的解法。对于一维数组,我们可以:
- 从左到右计算每个位置左侧所有元素的乘积(前缀积)
- 从右到左计算每个位置右侧所有元素的乘积(后缀积)
- 将对应位置的前后缀积相乘得到最终结果
对于二维矩阵,我们可以将其视为一维数组来处理。具体步骤:
- 将矩阵展平为一维数组
- 计算每个元素的前缀积和后缀积
- 将前后缀积相乘得到总乘积
- 将结果重新组织为矩阵形式
2.3 空间优化技巧
上述方法需要O(mn)的额外空间存储前后缀积。可以进一步优化:
- 先用结果矩阵存储前缀积
- 再用一个变量动态计算后缀积并直接乘到结果中
这样空间复杂度降为O(1)(不考虑输出矩阵)
3. 完整实现与代码解析
3.1 Python实现版本
python复制def construct_product_matrix(matrix):
if not matrix or not matrix[0]:
return []
m, n = len(matrix), len(matrix[0])
result = [[1]*n for _ in range(m)]
# 计算左上前缀积
for i in range(m):
for j in range(n):
if i == 0 and j == 0:
result[i][j] = 1
elif j == 0:
result[i][j] = result[i-1][-1] * matrix[i-1][-1]
else:
result[i][j] = result[i][j-1] * matrix[i][j-1]
# 计算右下后缀积并直接相乘
suffix = 1
for i in range(m-1, -1, -1):
for j in range(n-1, -1, -1):
result[i][j] *= suffix
suffix *= matrix[i][j]
return result
3.2 关键步骤解析
- 初始化阶段:创建与输入矩阵相同大小的结果矩阵,初始值全1
- 前缀积计算:
- 第一行第一列元素的前缀积为1
- 每行第一个元素的前缀积等于上一行最后一个元素的前缀积乘以上一行最后一个元素值
- 其他位置的前缀积等于左边元素的前缀积乘以左边元素值
- 后缀积计算:
- 从右下角开始向左上遍历
- 维护一个动态的suffix变量累积当前元素右侧所有元素的乘积
- 将suffix乘到当前位置的结果中
- 更新suffix时包含当前元素值
3.3 复杂度分析
- 时间复杂度:O(mn),只需两次完整遍历矩阵
- 空间复杂度:O(1)额外空间(不考虑输出矩阵)
4. 边界情况与测试用例
4.1 必须考虑的边界情况
- 空矩阵输入:应返回空矩阵
- 单元素矩阵:结果应为[[1]]
- 包含0的矩阵:需要特别注意,因为题目不允许使用除法
- 大数情况:乘积可能溢出,但Python整数不会溢出
4.2 测试用例示例
python复制test_cases = [
([], []), # 空矩阵
([[5]], [[1]]), # 单元素
([[0,1],[1,0]], [[0,0],[0,0]]), # 含0矩阵
([[1,2],[3,4]], [[24,12],[8,6]]), # 常规2x2
([[1,2,3],[4,5,6]], [[720,360,240],[180,144,120]]) # 题目示例
]
5. 常见错误与调试技巧
5.1 典型错误模式
- 索引越界:在处理第一行/列时容易忽略边界检查
- 解决方法:明确处理i=0和j=0的特殊情况
- 乘积初始化错误:前缀积初始值应为1而非0
- 遍历顺序错误:后缀积必须从右下向左上计算
- 空间浪费:不必要地存储完整的前后缀积数组
5.2 调试建议
- 对小矩阵打印中间结果:
python复制print("Prefix:", result) # 前缀积计算后 print("Final:", result) # 后缀积计算后 - 使用断言检查不变式:
python复制assert len(result) == len(matrix) assert all(len(row) == len(matrix[0]) for row in result) - 可视化矩阵遍历路径:
code复制
前缀积:→→→↓→→→↓ 后缀积:←←←↑←←←↑
6. 算法变种与扩展思考
6.1 允许使用除法的解法
如果允许使用除法(虽然题目禁止),可以:
- 计算所有元素乘积total_product
- 对每个元素,结果=total_product//matrix[i][j]
但需要注意:
- 要处理0的情况(0的个数超过1则全0,恰好1个则只有该位置不为0)
- 需要使用整数除法
6.2 并行计算优化
对于超大矩阵,可以考虑:
- 将矩阵分块
- 并行计算各块的前后缀积
- 合并结果时需要处理块边界处的乘积关系
6.3 实际应用场景
这种乘积矩阵的思想可用于:
- 图像处理中的局部滤波器计算
- 统计分析中排除自相关的计算
- 推荐系统中去除自身影响的评分预测
7. 力扣刷题通用技巧
7.1 矩阵类问题解题框架
- 确定遍历顺序(行优先/列优先/对角线)
- 考虑空间复用(是否可以在原矩阵上修改)
- 寻找子问题模式(如本题转化为一维问题)
- 处理边界条件(首行/末行/首列/末列)
7.2 调试复杂矩阵算法
- 使用小规模测试数据(2x2, 3x3)
- 打印中间状态矩阵
- 手工计算几个关键位置验证
- 使用可视化工具展示矩阵变化
7.3 时间复杂度优化思路
- 寻找重复计算部分(如本题的乘积计算)
- 考虑预处理(前缀和/积)
- 尝试空间换时间(存储中间结果)
- 利用矩阵特殊性质(对称性、稀疏性等)
8. 个人实战心得
这道题教会我最重要的一点是:二维问题往往可以降维到一维来解决。在实际编码时,我建议:
- 先在纸上画出小矩阵的计算过程,标注每个位置的乘积来源
- 初始版本可以不考虑空间优化,先实现正确性
- 使用矩阵展开技巧时,注意行优先和列优先的区别
- 对于Python,利用列表生成式可以简洁地初始化矩阵
我在第三次刷这道题时发现一个有趣的现象:如果将矩阵视为环形数组,还可以设计出另一种解法,但实现起来会更复杂。这提醒我们,有时候最直观的解法可能不是最优的,需要多角度思考问题。
