跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

前缀和算法:高效解决区间求和问题的关键

前缀和是一种通过预处理将区间查询复杂度降至 O(1) 的技术。核心在于构建累加数组,利用 dp[i] = dp[i-1] + nums[i] 快速计算任意区间的和。涵盖一维与二维前缀和模板,并延伸至哈希表结合的前缀和应用场景,如寻找中心下标、除自身以外乘积、和为 K 的子数组及矩阵区域和等问题。重点解析了边界处理、负数取模修正及空间优化技巧,帮助读者掌握从基础模板到复杂变体的解题思路。

FlinkHero发布于 2026/3/26更新于 2026/10/24103 浏览
前缀和算法:高效解决区间求和问题的关键

前缀和算法详解

前缀和是一种通过预处理将区间查询复杂度降至 O(1) 的技术。核心在于构建累加数组,利用 dp[i] = dp[i-1] + nums[i] 快速计算任意区间的和。本文涵盖一维与二维前缀和模板,并延伸至哈希表结合的前缀和应用场景,如寻找中心下标、除自身以外乘积、和为 K 的子数组及矩阵区域和等问题。

DP34.【模板】一维前缀和

题目描述:给定一个数组,多次询问某个区间的元素和。

暴力解法 vs 前缀和

暴力解法每次询问都遍历区间,时间复杂度为 O(n*q)。而使用前缀和预处理后,单次查询只需 O(1),总复杂度降为 O(q+n)。

实现思路

第一步是预处理前缀和数组。dp[i] 表示 [1, i] 区间内所有元素的和。递推公式很简单:

dp[i] = dp[i - 1] + arr[i]

第二步是利用前缀和数组快速计算区间和。当询问区间为 [l, r] 时,区间和等于 dp[r] - dp[l - 1]。

注意:C++ 中 vector<int> dp(n + 1) 默认初始化为 0,因此 dp[0] 也是 0,这正好符合边界条件。

代码实现

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, q;
    cin >> n >> q;
    
    // 输入原始数组,下标从 1 开始方便处理
    vector<int> arr(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
    }

    // 预处理前缀和数组,使用 long long 防止溢出
    vector<long long> dp(n + 1);
    for (int i = 1; i <= n; i++) {
        dp[i] = dp[i - ] + arr[i];
    }

    
     (q--) {
         l, r;
        cin >> l >> r;
        cout << dp[r] - dp[l - ] << endl;
    }
     ;
}
1
// 处理查询
while
int
1
return
0

细节问题:为什么下标从 1 开始?

如果下标从 0 开始,计算 dp[l-1] 时当 l=0 会出现负数下标越界。通过添加虚拟节点(辅助节点),让有效数据从下标 1 开始,dp[0] 作为基准值 0,可以完美规避边界检查,简化逻辑。


DP35.【模板】二维前缀和

题目描述:给定一个矩阵,多次询问子矩阵的元素和。

算法思路

二维前缀和是一维的扩展。dp[i][j] 表示从 [1, 1] 到 [i, j] 这个矩形区域内所有元素的和。

状态转移方程推导如下:

dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + arr[i][j]

这里减去 dp[i-1][j-1] 是因为它在加 dp[i-1][j] 和 dp[i][j-1] 时被重复计算了两次。

查询时,对于矩形区域 [x1, y1] 到 [x2, y2],利用容斥原理:

sum = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1]

代码实现

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m, q;
    cin >> n >> m >> q;

    // 读入数据,下标从 1 开始
    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] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + arr[i][j];
        }
    }

    // 查询
    while (q--) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1] << endl;
    }
    return 0;
}

724. 寻找数组的中心下标

这道题要求找到一个下标,使得该下标左侧元素之和等于右侧元素之和。

思路分析

我们可以分别构建前缀和数组和后缀和数组。f[i] 表示 [0, i-1] 的和,g[i] 表示 [i+1, n-1] 的和。当 f[i] == g[i] 时,i 即为中心下标。

需要注意的是,f[0] 和 g[n-1] 都为 0,因为边界外没有元素。如果有多个中心下标,返回最左边的一个即可。

代码实现

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;
    }
};

238. 除自身以外数组的乘积

这道题不能用除法,且要求 O(n) 时间复杂度。思路与上题类似,只是将加法换成了乘法。

我们需要构建前缀积 f[i] 和后缀积 g[i]。f[i] 表示 nums[0] 到 nums[i-1] 的乘积,g[i] 表示 nums[i+1] 到 nums[n-1] 的乘积。最终结果 ret[i] = f[i] * g[i]。

注意初始化:f[0] 和 g[n-1] 应设为 1,因为乘法单位元是 1。

代码实现

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n), g(n);
        
        f[0] = 1;
        g[n - 1] = 1;
        
        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];
        }
        
        vector<int> ret(n);
        for (int i = 0; i < n; i++) {
            ret[i] = f[i] * g[i];
        }
        return ret;
    }
};

560. 和为 K 的子数组

这类问题通常用「前缀和 + 哈希表」优化。核心思想是:如果 sum[i] - sum[j] = k,那么区间 [j+1, i] 的和就是 k。这等价于寻找有多少个前缀和等于 sum[i] - k。

关键点

  1. 哈希表存储:hash[val] 记录前缀和为 val 出现的次数。
  2. 初始化:hash[0] = 1,代表前缀和为 0 的情况出现了一次(对应空数组),这样当子数组本身和为 k 时也能被统计到。
  3. 无需显式数组:可以直接在遍历时维护当前前缀和,不需要额外创建前缀和数组。

代码实现

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        int sum = 0, ret = 0;
        hash[0] = 1; // 确保第一个数能被正确统计
        
        for (auto x : nums) {
            sum += x;
            if (hash.count(sum - k)) {
                ret += hash[sum - k];
            }
            hash[sum]++;
        }
        return ret;
    }
};

974. 和可被 K 整除的子数组

本题与上一题类似,但需要判断子数组和是否能被 K 整除。根据同余定理,(a - b) % k == 0 等价于 a % k == b % k。

难点:负数取模

在 C++ 和 Java 中,负数取模的结果可能为负。例如 -1 % 5 结果是 -1。为了统一,修正公式为 (sum % k + k) % k。

代码实现

class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        hash[0 % k]++; // 处理 0 的余数情况
        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;
    }
};

525. 连续数组

题目要求找到最长的连续子数组,其中 0 和 1 的数量相等。可以将 0 视为 -1,1 视为 1,问题转化为寻找和为 0 的最长子数组。

细节处理

  1. 哈希表存储下标:hash[sum] = i,记录前缀和第一次出现的位置。
  2. 默认前缀和:设置 hash[0] = -1,表示前缀和为 0 时的虚拟下标是 -1,这样计算长度 i - (-1) 时能正确包含从开头开始的子数组。
  3. 最长长度:遇到相同前缀和时,保留最早的下标以获取最大长度。

代码实现

class Solution {
public:
    int findMaxLength(vector<int>& nums) {
        unordered_map<int, int> hash;
        hash[0] = -1; // 默认有一个前缀和为 0 的情况
        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;
    }
};

1314. 矩阵区域和

这道题结合了二维前缀和与范围限制。需要计算每个位置周围 2k+1 范围内的元素和。

映射关系

由于 dp 表索引从 1 开始,而原矩阵 mat 是 0-based,所以在计算边界时需要做偏移。x1, x2, y1, y2 都需要加 1 来适配 dp 表。

同时要注意边界保护,确保坐标不越界,使用 max 和 min 函数限制范围。

代码实现

class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        
        // 创建 dp 表,多开一行一列便于处理边界
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        
        // 填充 dp 表数据
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                dp[i][j] = dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        
        vector<vector<int>> answer(m, vector<int>(n));
        int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 计算查询区域的边界,注意 +1 适配 dp 表
                x1 = max(i - k, 0) + 1;
                y1 = max(j - k, 0) + 1;
                x2 = min(i + k, m - 1) + 1;
                y2 = min(j + k, n - 1) + 1;
                
                answer[i][j] = dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1];
            }
        }
        return answer;
    }
};

掌握前缀和的关键在于理解其'空间换时间'的本质,以及在不同维度(一维、二维)和不同运算(加、乘、模)下的变体应用。实际编码时,务必注意边界条件和数据类型溢出问题。

目录

  1. 前缀和算法详解
  2. DP34.【模板】一维前缀和
  3. 暴力解法 vs 前缀和
  4. 实现思路
  5. 代码实现
  6. 细节问题:为什么下标从 1 开始?
  7. DP35.【模板】二维前缀和
  8. 算法思路
  9. 代码实现
  10. 724. 寻找数组的中心下标
  11. 思路分析
  12. 代码实现
  13. 238. 除自身以外数组的乘积
  14. 代码实现
  15. 560. 和为 K 的子数组
  16. 关键点
  17. 代码实现
  18. 974. 和可被 K 整除的子数组
  19. 难点:负数取模
  20. 代码实现
  21. 525. 连续数组
  22. 细节处理
  23. 代码实现
  24. 1314. 矩阵区域和
  25. 映射关系
  26. 代码实现

更多推荐文章

查看全部
  • React 19 + Vite + TypeScript 项目初始化与配置实战
  • MCP、Agent 与 Skills:AI 时代三大核心概念解析
  • Effective Modern C++ 第 37 条:确保 std::thread 析构前不可结合
  • OpenClaw Gateway 命令与交互指南
  • 开源轻小说机翻机器人:从部署到架构解析
  • 使用 Bright Data Web Scraper API 配合 Python 抓取 Glassdoor 数据
  • C++ 二叉搜索树:增删查改与 key/value 场景实现
  • llama.cpp加载多模态gguf模型
  • 基于 Go 的电子病历智能助手与 HIS 对接实战:AI 临床副驾驶构建
  • MVP 到千万级并发:AI 在前后端开发中的差异化落地指南
  • FAIR plus 2026 机器人全产业链接会前瞻
  • VSCode GitHub Copilot 安装与使用指南
  • Spring Cloud + AI:微服务架构下的智能路由、故障自愈、日志分析
  • 无人机避障算法核心技术:五种主流算法原理与实战应用场景
  • Vue3 前端面试核心 20 题详解(含代码实战)
  • VS Code 前端开发必备插件推荐:10 款工具提升效率及配置
  • 机器人脑部药物递送三大技术路径的可转化性分析
  • 发那科机器人与西门子 PLC 通讯方案(网关+Modbus TCP)
  • 2026 年主流 AIGC 长文本写作工具实测:5 款头部工具优缺点与场景适配
  • 2023 年网络安全十大发展趋势深度解析

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online