【算法题】前缀和

前缀和是一种预处理优化技术 ,核心思想是通过提前计算"前缀累计值",将数组/矩阵的区间查询操作 从暴力遍历的 O(n)O(n)O(n) 优化到 O(1)O(1)O(1),同时也能结合哈希表解决"子数组和"等复杂问题。

它的应用场景非常广泛:既可以处理基础的"多次区间和查询",也能扩展到二维矩阵的区域和,还能结合哈希表解决"子数组和为目标值""最长平衡子数组"等问题。本文将通过8道经典题目,拆解前缀和在不同场景下的解题逻辑与代码实现。

一、一维前缀和(模板题)

题目描述:

给定长度为 n 的数组和 m 次查询,每次查询给出区间 [l, r],输出该区间内元素的和。

示例

  • 输入:3 2 → 数组 [1,2,4] → 查询 [1,2][2,3]
  • 输出:36

解题思路:

预处理"前缀和数组"dp,其中 dp[i] 表示前 i 个元素的累加和(数组下标从1开始,方便边界处理)。

  • 前缀和公式:dp[i] = dp[i-1] + arr[i]
  • 区间 [l, r] 的和:dp[r] - dp[l-1](用前r个和减去前l-1个和,得到中间区间的和)

完整代码:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    vector<int> arr(n + 1);
    for(int i = 1; i <= n; i++) cin >> arr[i];

    vector<long long> dp(n + 1);
    for(int i = 1; i <= n; i++) dp[i] = dp[i - 1] + arr[i];

    int l = 0, r = 0;
    while(m--) 
    {
        cin >> l >> r;
        cout << dp[r] - dp[l - 1] << endl;
    }

    return 0;
}

复杂度分析:

  • 时间复杂度:预处理 O(n)O(n)O(n),每次查询 O(1)O(1)O(1),总复杂度 O(n+m)O(n + m)O(n+m)。
  • 空间复杂度:O(n)O(n)O(n),存储前缀和数组。

二、二维前缀和(模板题)

题目描述:

给定 n×m 的矩阵和 q 次查询,每次查询给出子矩阵的左上角 (x1,y1) 和右下角 (x2,y2),输出该子矩阵内元素的和。

示例

  • 输入:3 4 3 → 矩阵 [[1,2,3,4],[3,2,1,0],[1,5,7,8]] → 查询 (1,1,2,2)
  • 输出:82532

解题思路:

预处理"二维前缀和数组"dp,其中 dp[i][j] 表示以 (1,1) 为左上角、(i,j) 为右下角的子矩阵和。

  • 二维前缀和公式:dp[i][j] = arr[i][j] + dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1](加当前元素,加左、上区域和,减重复计算的左上区域和)
  • 子矩阵和公式:dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1](减左、上区域,加回重复减去的左上区域)

完整代码:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m, q;
    cin >> n >> m >> q;
    vector<vector<int>> arr(n + 1, vector<int>(m + 1));
    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
            cin >> arr[i][j];

    vector<vector<long long>> dp(n + 1, vector<long long>(m + 1));
    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
            dp[i][j] = arr[i][j] + dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1];
    
    int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
    while(q--)
    {
        cin >> x1 >> y1 >> x2 >> y2;
        cout << dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1] << endl;
    }
}

复杂度分析:

  • 时间复杂度:预处理 O(n×m)O(n×m)O(n×m),每次查询 O(1)O(1)O(1),总复杂度 O(n×m+q)O(n×m + q)O(n×m+q)。
  • 空间复杂度:O(n×m)O(n×m)O(n×m),存储二维前缀和数组。

三、寻找数组的中心下标

题目描述:

数组的中心下标 是指其左侧所有元素和等于右侧所有元素和的下标(若在两端,对应侧和视为0)。返回最靠左的中心下标,不存在则返回 -1

示例

  • 输入:nums = [1,7,3,6,5,6],输出:3(左侧和 1+7+3=11,右侧和 5+6=11

解题思路:

预处理前缀和 (左到右累加)和后缀和(右到左累加):

  1. 前缀和数组 ff[i]nums[0..i-1] 的和(i 的左侧和)。
  2. 后缀和数组 gg[i]nums[i+1..n-1] 的和(i 的右侧和)。
  3. 遍历每个下标 i,判断 f[i] == g[i],返回第一个满足条件的下标。

完整代码:

cpp 复制代码
class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n), g(n);

        for(int i = 1; i < n; i++) 
            f[i] = f[i - 1] + nums[i - 1];
        for(int i = n - 2; i >= 0; i--)
            g[i] = g[i + 1] + nums[i + 1]; 

        for(int i = 0; i < n; i++)
            if(f[i] == g[i])
                return i;
        return -1;
    }
};

复杂度分析:

  • 时间复杂度:O(n)O(n)O(n),预处理前缀和、后缀和及遍历各占 O(n)O(n)O(n)。
  • 空间复杂度:O(n)O(n)O(n),存储前缀和、后缀和数组。

四、除自身以外数组的乘积

题目描述:

返回数组 answer,其中 answer[i]nums 中除 nums[i] 外其余元素的乘积,要求不使用除法且时间复杂度为 O(n)O(n)O(n)。

示例

  • 输入:nums = [1,2,3,4],输出:[24,12,8,6]

解题思路:

类比前缀和,预处理前缀积后缀积

  1. 前缀积数组 ff[i]nums[0..i-1] 的乘积(i 左侧元素的乘积)。
  2. 后缀积数组 gg[i]nums[i+1..n-1] 的乘积(i 右侧元素的乘积)。
  3. 每个位置的结果:answer[i] = f[i] * g[i]

完整代码:

cpp 复制代码
class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n), g(n);
        f[0] = g[n - 1] = 1;
        vector<int> ret(n);

        for(int i = 1; i < n; i++)
            f[i] = f[i - 1] * nums[i - 1];
        for(int i = n - 2; i >= 0; i--)
            g[i] = g[i + 1] * nums[i + 1];

        for(int i = 0; i < n; i++)
            ret[i] = f[i] * g[i];
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:O(n)O(n)O(n),预处理前缀积、后缀积及遍历各占 O(n)O(n)O(n)。
  • 空间复杂度:O(n)O(n)O(n),存储前缀积、后缀积数组(可优化为 O(1)O(1)O(1),用结果数组临时存储)。

五、和为 K 的子数组

题目描述:

统计数组中和为 k连续子数组的个数。

示例

  • 输入:nums = [1,1,1], k = 2,输出:2(子数组 [1,1] 出现两次)

解题思路:

前缀和 + 哈希表优化:

  1. 定义前缀和 sum:遍历到当前元素的累加和。
  2. 哈希表 hash 存储"前缀和出现的次数"(初始存入 {0:1},对应前缀和为0的情况)。
  3. 遍历数组时,若 sum - k 存在于哈希表中,说明存在以当前元素结尾、和为 k 的子数组,累加其出现次数;最后将当前 sum 加入哈希表。

完整代码:

cpp 复制代码
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        hash[0] = 1;

        int sum = 0, ret = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            sum += nums[i];
            if(hash.count(sum - k)) ret += hash[sum - k];
            hash[sum]++;
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:O(n)O(n)O(n),遍历数组一次,哈希表操作均为 O(1)O(1)O(1)。
  • 空间复杂度:O(n)O(n)O(n),哈希表最多存储 nnn 个不同的前缀和。

六、和可被 K 整除的子数组

题目描述:

统计数组中和可被 k 整除的连续子数组的个数。

示例

  • 输入:nums = [4,5,0,-2,-3,1], k = 5,输出:7

解题思路:

利用同余性质 :若两个前缀和 sum1sum2k 取模的结果相同,则 sum2 - sum1 可被 k 整除。

  1. 前缀和 sum 遍历累加,计算其对 k 的模 r(处理负数模:(sum % k + k) % k)。
  2. 哈希表 hash 存储"模值出现的次数"(初始存入 {0:1})。
  3. 遍历数组时,若 r 存在于哈希表中,累加其出现次数;最后将当前 r 加入哈希表。

完整代码:

cpp 复制代码
class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        hash[0] = 1;

        int sum = 0, ret = 0;
        for(auto x : nums)
        {
            sum += x;
            int r = (sum % k + k) % k;
            if(hash.count(r)) ret += hash[r];
            hash[r]++;
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:O(n)O(n)O(n),遍历数组一次,哈希表操作均为 O(1)O(1)O(1)。
  • 空间复杂度:O(k)O(k)O(k),模值范围为 0~k-1,哈希表最多存储 k 个键。

七、连续数组

题目描述:

给定二进制数组,找到包含相同数量 01最长连续子数组,返回其长度。

示例

  • 输入:nums = [0,1,0],输出:2(子数组 [0,1][1,0]

解题思路:

0 转化为 -1,问题转化为"寻找和为 0 的最长连续子数组",再用前缀和 + 哈希表

  1. 前缀和 sum 遍历累加(0-111)。
  2. 哈希表 hash 存储"前缀和第一次出现的下标"(初始存入 {0: -1},对应前缀和为0的初始状态)。
  3. 遍历数组时,若 sum 已存在,计算当前下标与第一次出现下标的差,更新最长长度;若不存在,存入当前下标。

完整代码:

cpp 复制代码
class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        unordered_map<int, int> hash;
        hash[0] = -1;

        int sum = 0, ret = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            sum += nums[i] == 0 ? -1 : 1;
            if(hash.count(sum)) ret = max(ret, i - hash[sum]);
            else hash[sum] = i;
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:O(n)O(n)O(n),遍历数组一次,哈希表操作均为 O(1)O(1)O(1)。
  • 空间复杂度:O(n)O(n)O(n),哈希表最多存储 nnn 个不同的前缀和。

八、矩阵区域和

题目描述:

给定 m×n 矩阵 mat 和整数 k,返回矩阵 answer,其中 answer[i][j]mat 中以 (i,j) 为中心、边长为 2k+1 的正方形区域内的元素和(区域需在矩阵内)。

示例

  • 输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1,输出:[[12,21,16],[27,45,33],[24,39,28]]

解题思路:

结合二维前缀和

  1. 预处理二维前缀和数组 dp(同"二维前缀和模板")。
  2. 遍历每个位置 (i,j),确定查询区域的边界:
    • 左上角 (x1, y1)max(0, i-k) + 1(加1是因为前缀和数组下标从1开始)
    • 右下角 (x2, y2)min(m-1, i+k) + 1
  3. 用二维前缀和公式计算该区域的和,存入 answer[i][j]

完整代码:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
            {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + mat[i - 1][j - 1];
            }

        vector<vector<int>> ret(m, vector<int>(n));
        for(int i = 0; i < m; i++)
            for(int j = 0; j < n; j++)
            {
                int x1 = max(0, i - k) + 1, y1 = max(0, j - k) + 1;
                int x2 = min(m - 1, i + k) + 1, y2 = min(n - 1, j + k) + 1;
                ret[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
            }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:预处理 O(m×n)O(m×n)O(m×n),遍历计算结果 O(m×n)O(m×n)O(m×n),总复杂度 O(m×n)O(m×n)O(m×n)。
  • 空间复杂度:O(m×n)O(m×n)O(m×n),存储二维前缀和数组。
相关推荐
bbq粉刷匠2 小时前
Java--二叉树概念及其基础应用
java·数据结构·算法
高洁012 小时前
知识图谱构建
人工智能·深度学习·算法·机器学习·知识图谱
郝亚军2 小时前
顺序栈C语言版本
c语言·开发语言·算法
AndrewHZ2 小时前
【图像处理基石】什么是神经渲染?
图像处理·人工智能·神经网络·算法·cnn·计算机图形学·神经渲染
2401_841495642 小时前
【LeetCode刷题】爬楼梯
数据结构·python·算法·leetcode·动态规划·滑动窗口·斐波那契数列
byzh_rc2 小时前
[模式识别-从入门到入土] 组合分类器
人工智能·算法·机器学习·支持向量机·概率论
没有故事的Zhang同学2 小时前
04-📦数据结构与算法核心知识 | 动态数组:理论与实践的系统性研究
算法
炽烈小老头2 小时前
【每天学习一点算法 2025/12/25】爬楼梯
学习·算法·动态规划
睡醒了叭2 小时前
图像分割-传统算法-阈值分割原理与实践
opencv·算法·计算机视觉