LeetCode 每日一题笔记
0. 前言
- 日期:2025.03.24
- 题目:2906.构造乘积矩阵
- 难度:中等
- 标签:数组 矩阵 前缀和
1. 题目理解
问题描述
给你一个下标从 0 开始、大小为 n * m 的二维整数矩阵 grid,定义一个下标从 0 开始、大小为 n * m 的二维矩阵 p。如果满足以下条件,则称 p 为 grid 的 乘积矩阵:
- 对于每个元素
p[i][j],它的值等于除了grid[i][j]外所有元素的乘积,且乘积对12345取余数。
返回grid的乘积矩阵。
示例
输入:grid = [[1,2],[3,4]]
输出:[[24,12],[8,6]]
解释:
p[0][0] = grid[0][1] * grid[1][0] * grid[1][1] = 2 * 3 * 4 = 24p[0][1] = grid[0][0] * grid[1][0] * grid[1][1] = 1 * 3 * 4 = 12p[1][0] = grid[0][0] * grid[0][1] * grid[1][1] = 1 * 2 * 4 = 8p[1][1] = grid[0][0] * grid[0][1] * grid[1][0] = 1 * 2 * 3 = 6
2. 解题思路
核心观察
- 暴力求解的问题 :若直接对每个元素
grid[i][j],遍历所有元素计算乘积(排除自身),时间复杂度为 O((n∗m)2)O((n*m)^2)O((n∗m)2)。当矩阵规模较大时(如 n、m 为 1000),计算量会达到 101210^{12}1012 级别,远超时间限制,且多次乘法会导致数值溢出(即使取模也无法降低时间复杂度)。 - 前缀/后缀乘积优化 :将二维矩阵展平为一维数组,通过「前缀乘积数组」和「后缀乘积数组」快速计算每个位置排除自身的总乘积:
- 前缀乘积
pre[i]:表示一维数组中前i个元素的乘积(包含i); - 后缀乘积
suf[i]:表示一维数组中从i到末尾的乘积(包含i); - 对于位置
k,排除自身的总乘积 = 前缀乘积pre[k-1]* 后缀乘积suf[k+1](边界位置单独处理)。
- 前缀乘积
算法步骤
- 展平矩阵 :将二维矩阵
grid转换为一维数组arr,便于统一计算前缀/后缀乘积; - 计算前缀乘积 :
pre[0] = arr[0],pre[i] = (pre[i-1] * arr[i]) % 12345; - 计算后缀乘积 :
suf[len-1] = arr[len-1],suf[i] = (suf[i+1] * arr[i]) % 12345; - 重构结果矩阵:遍历每个位置,根据前缀/后缀乘积计算排除自身的总乘积,转换回二维矩阵并取模。
3. 代码实现
java
package com.sheeta1998.lec.lc2906;
class Solution {
public int[][] constructProductMatrix(int[][] grid) {
int n = grid.length; // 矩阵行数
int m = grid[0].length; // 矩阵列数
int total = n * m; // 一维数组总长度
int[] arr = new int[total];
int[] pre = new int[total]; // 前缀乘积数组
int[] suf = new int[total]; // 后缀乘积数组
// 步骤1:将二维矩阵展平为一维数组
int idx = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
arr[idx++] = grid[i][j];
}
}
// 步骤2:计算前缀乘积
pre[0] = arr[0] % 12345;
for (int i = 1; i < total; i++) {
pre[i] = (pre[i-1] * arr[i]) % 12345;
}
// 步骤3:计算后缀乘积
suf[total-1] = arr[total-1] % 12345;
for (int i = total-2; i >= 0; i--) {
suf[i] = (suf[i+1] * arr[i]) % 12345;
}
// 步骤4:重构乘积矩阵
idx = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (idx == 0) {
// 第一个元素:只有后缀乘积
grid[i][j] = suf[1] % 12345;
} else if (idx == total-1) {
// 最后一个元素:只有前缀乘积
grid[i][j] = pre[total-2] % 12345;
} else {
// 中间元素:前缀*后缀
grid[i][j] = (pre[idx-1] * suf[idx+1]) % 12345;
}
idx++;
}
}
return grid;
}
}
4. 代码优化说明
- 空间优化 :原代码中
prearr/sufarr命名简化为pre/suf,变量名更直观; - 取模时机 :每一步乘法后立即对
12345取模,避免整数溢出(Java 中 int 最大值约 2×1092×10^92×109,多次乘法易溢出); - 边界处理:明确区分第一个/最后一个元素的计算逻辑,避免数组越界;
- 变量复用 :直接修改原矩阵
grid存储结果,无需额外创建二维数组,节省空间。
5. 复杂度分析
- 时间复杂度 :O(n×m)O(n×m)O(n×m)。展平矩阵、计算前缀/后缀乘积、重构矩阵均为一次遍历,总次数为 3×n×m3×n×m3×n×m,属于线性时间复杂度;
- 空间复杂度 :O(n×m)O(n×m)O(n×m)。需要额外存储一维数组
arr、前缀数组pre、后缀数组suf,总空间为 3×n×m3×n×m3×n×m(可进一步优化为 O(1)O(1)O(1) 空间,直接在二维矩阵上计算前缀/后缀,但代码可读性降低)。
6. 总结
- 暴力求解不可行的原因 :时间复杂度为 O((n×m)2)O((n×m)^2)O((n×m)2),矩阵规模较大时会超时,且多次乘法易导致数值溢出;
- 核心优化思路:利用「前缀/后缀乘积」将时间复杂度降至线性,通过展平二维矩阵简化计算逻辑;
- 关键细节:每一步乘法后取模避免溢出,边界位置单独处理防止数组越界。
关键点回顾
- 暴力解法因 O((n∗m)2)O((n*m)^2)O((n∗m)2) 时间复杂度无法通过大规模用例,必须用前缀/后缀乘积优化;
- 展平二维矩阵是简化前缀/后缀计算的核心技巧,最终需还原为二维结果;
- 取模操作需在每一步乘法后执行,避免整数溢出且满足题目要求。