LeetCode 238 & 2906.构造乘积数组与乘积矩阵

LeetCode 238 & 2906.构造乘积数组与乘积矩阵 从一维到二维:前缀积与后缀积的妙用

前言

在算法题中,有一类经典问题:给定一个数组(或矩阵),计算每个位置除自身以外所有元素的乘积

如果直接使用除法,虽然思路简单,但需要处理元素为 0 的情况,并且题目通常禁止使用除法。

本文将深入解析两题:

  • LeetCode 238. 除自身以外数组的乘积(一维)
  • LeetCode 2906. 构造乘积矩阵(二维)

通过对比,展示如何利用前缀积 × 后缀积的技巧,以 O(n) 或 O(n×m) 的时间复杂度优雅求解。


一、LeetCode 238:除自身以外数组的乘积

题目描述

给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
要求:不使用除法,且在 O(n) 时间复杂度内完成。

解法思路

answer[i] 分解为两部分:

  • 左边所有元素的乘积(前缀积)
  • 右边所有元素的乘积(后缀积)

因此,我们可以先计算每个位置的前缀积,再乘以后缀积,得到最终结果。

代码实现(C++)

cpp 复制代码
class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> pre(n, 1);          // 结果数组,先存储前缀积
        // 计算前缀积
        for (int i = 1; i < n; ++i) {
            pre[i] = pre[i - 1] * nums[i - 1];
        }
        int suf = 1;
        // 从右向左乘以后缀积
        for (int i = n - 1; i >= 0; --i) {
            pre[i] *= suf;               // 左边乘积 × 右边乘积
            suf *= nums[i];              // 更新后缀积
        }
        return pre;
    }
};

模拟过程(以 nums = [1, 2, 3, 4] 为例)

步骤 操作 pre 数组变化 后缀积 suf
初始 [1, 1, 1, 1] ---
前缀积 i=1: pre[1] = 1×1=1 [1, 1, 1, 1]
i=2: pre[2] = 1×2=2 [1, 1, 2, 1]
i=3: pre[3] = 2×3=6 [1, 1, 2, 6]
后缀积 suf=1 1
i=3: pre[3]=6×1=6; suf=1×4=4 [1, 1, 2, 6] 4
i=2: pre[2]=2×4=8; suf=4×3=12 [1, 1, 8, 6] 12
i=1: pre[1]=1×12=12; suf=12×2=24 [1, 12, 8, 6] 24
i=0: pre[0]=1×24=24; suf=24×1=24 [24, 12, 8, 6] 24

最终结果 [24, 12, 8, 6] 符合预期。


二、LeetCode 2906:构造乘积矩阵

题目描述

给你一个二维整数矩阵 grid,构造一个同样大小的矩阵 p,使得
p[i][j] 等于矩阵中除 grid[i][j] 以外所有元素的乘积,结果对 12345 取模。
要求:不能使用除法。

解法思路

将二维矩阵按行优先展开为一维视角,本质与一维完全相同。

我们将每个位置 (i, j) 的乘积拆分为:

  • 前缀积:所有行号 < i 的格子,以及同一行中列号 < j 的格子(即左上部分)
  • 后缀积:所有行号 > i 的格子,以及同一行中列号 > j 的格子(即右下部分)

两次遍历:

  1. 逆序遍历(从右下角向左上角),计算每个位置的后缀积,暂存在结果矩阵中。
  2. 正序遍历(从左到右、从上到下),同时维护前缀积,与之前暂存的后缀积相乘,得到最终结果。

代码实现(C++)

cpp 复制代码
class Solution {
public:
    vector<vector<int>> constructProductMatrix(vector<vector<int>>& grid) {
        constexpr int MOD = 12345;
        int n = grid.size(), m = grid[0].size();
        vector<vector<int>> p(n, vector<int>(m));

        // 第一遍:后缀积(右侧 + 下侧)
        long long suf = 1;
        for (int i = n - 1; i >= 0; --i) {
            for (int j = m - 1; j >= 0; --j) {
                p[i][j] = suf;
                suf = suf * grid[i][j] % MOD;
            }
        }

        // 第二遍:乘以前缀积(左侧 + 上侧)
        long long pre = 1;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                p[i][j] = p[i][j] * pre % MOD;
                pre = pre * grid[i][j] % MOD;
            }
        }

        return p;
    }
};

模拟过程(以 grid = [[1,2],[3,4]] 为例)

初始矩阵

复制代码
1 2
3 4

第一步:逆序遍历,计算后缀积(suf 初始 1)

位置 (i,j) 操作 结果矩阵 p suf 更新
(1,1) p[1][1] = 1 [[?,?],[?,1]] suf = 1×4 = 4
(1,0) p[1][0] = 4 [[?,?],[4,1]] suf = 4×3 = 12
(0,1) p[0][1] = 12 [[?,12],[4,1]] suf = 12×2 = 24
(0,0) p[0][0] = 24 [[24,12],[4,1]] suf = 24×1 = 24

此时 p 存储的是每个位置右侧及下侧所有元素的乘积(不含自身)。

第二步:正序遍历,乘以前缀积(pre 初始 1)

位置 (i,j) 操作 结果矩阵 p 变化 pre 更新
(0,0) p[0][0] = 24×1 = 24 [[24,12],[4,1]] pre = 1×1 = 1
(0,1) p[0][1] = 12×1 = 12 [[24,12],[4,1]] pre = 1×2 = 2
(1,0) p[1][0] = 4×2 = 8 [[24,12],[8,1]] pre = 2×3 = 6
(1,1) p[1][1] = 1×6 = 6 [[24,12],[8,6]] pre = 6×4 = 24

最终结果:

复制代码
24 12
8  6

验证:

  • (0,0) 除自身乘积 = 2×3×4 = 24 ✓
  • (0,1) 除自身乘积 = 1×3×4 = 12 ✓
  • (1,0) 除自身乘积 = 1×2×4 = 8 ✓
  • (1,1) 除自身乘积 = 1×2×3 = 6 ✓

三、两题的共通点与技巧总结

1. 核心思想:前缀积 × 后缀积

无论是数组还是矩阵,都能将"除自身外所有元素的乘积"分解为:

复制代码
结果 = 左边/上边所有元素的乘积 × 右边/下边所有元素的乘积

通过两次遍历分别计算出这两部分,并相乘得到最终答案。

2. 空间优化

  • LeetCode 238:直接将结果数组作为前缀积存储,再通过一个变量维护后缀积,实现 O(1) 额外空间。
  • LeetCode 2906:同样将结果矩阵作为后缀积的暂存,再乘以前缀积,额外空间只有几个变量。

3. 避免除法

题目明确禁止使用除法,且元素可能包含 0。本方法完全规避了除法,通过乘法累积完成。

4. 模运算

LeetCode 2906 要求对 12345 取模。在乘法过程中及时取模,防止溢出,并保证结果在 int 范围内。

5. 遍历顺序

  • 前缀积通常采用正向遍历(从左到右、从上到下)。
  • 后缀积采用反向遍历(从右到左、从下到上)。

四、复杂度分析

题目 时间复杂度 空间复杂度(不计输出数组)
LeetCode 238 O(n) O(1)
LeetCode 2906 O(n×m) O(1)

两算法均只遍历两次原数组/矩阵,且只使用常数个额外变量,非常高效。


五、总结

通过对比一维和二维的"除自身外乘积"问题,我们看到了同一思想在不同数据结构上的迁移能力。

  • 核心技巧:前缀积 + 后缀积
  • 优势:无需除法,时间空间复杂度优秀,易于扩展
  • 适用场景:任何需要计算"除自身外其他元素积"的场景,都可以考虑本方法。
相关推荐
玲娜贝儿--努力学习买大鸡腿版1 小时前
hot 100 刷题记录(1)
数据结构·python·算法
123过去1 小时前
pixiewps使用教程
linux·网络·测试工具·算法·哈希算法
深圳市快瞳科技有限公司1 小时前
低空经济下,鸟类识别算法与无人机硬件的兼容性优化策略
算法·无人机
努力中的编程者1 小时前
二叉树(C语言底层实现)
c语言·开发语言·数据结构·c++·算法
鹤旗2 小时前
While语句,do-while语句,for语句
java·jvm·算法
qq_416018722 小时前
高性能密码学库
开发语言·c++·算法
NAGNIP2 小时前
一文搞懂卷积神经网络经典架构-LeNet
算法·面试
宵时待雨2 小时前
C++笔记归纳14:AVL树
开发语言·数据结构·c++·笔记·算法
NAGNIP2 小时前
一文搞懂深度学习中的池化!
算法·面试
山川行2 小时前
关于《项目C语言》专栏的总结
c语言·开发语言·数据结构·vscode·python·算法·visual studio code