C++算法学习专题:前缀和

今天我们来学习新的C++算法思想:前缀和

相关题解代码已经上传至作者的个人gitee:CPP 学习代码库: C++代码库新库,旧有C++仓库满员了喜欢请支持以下谢谢

目录

前缀和算法详解

基本概念

构建方法

应用场景

[1. 快速区间求和](#1. 快速区间求和)

[2. 解决子数组相关问题](#2. 解决子数组相关问题)

[3. 多维前缀和](#3. 多维前缀和)

复杂度分析

变种与应用示例

差分数组

1、前缀和

2、二维前缀和

3、寻找数组的中心下标

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

5、和为k的子数组

6、和可以被k整除的子数组(某一年蓝桥杯原题)

同余定理:

C++/Java中负数%正数的结果及其修正:

[C++ 中的取模运算](#C++ 中的取模运算)

[Java 中的取模运算](#Java 中的取模运算)

数学上的期望行为

针对特定情况的简化修正

[使用标准库函数(C++17 及以上)](#使用标准库函数(C++17 及以上))

7、连续数组

8、矩阵区域和


前缀和算法详解

前缀和(Prefix Sum)是一种常用的预处理技术,用于高效处理数组区间求和问题。通过预先计算并存储部分和,可以将区间查询的时间复杂度从O(n)降低到O(1)。

基本概念

前缀和数组定义为:对于给定数组arr,其前缀和数组prefix中,prefix[i]表示arr[0]arr[i-1]的元素之和(有些实现中可能包含arr[i]本身,具体取决于实现方式)。

构建方法

  1. 初始化一个与原数组等长的前缀和数组prefix
  2. prefix[0] = arr[0](或0,取决于边界处理)
cpp 复制代码
原数组: [1, 2, 3, 4, 5]
前缀和数组: [0, 1, 3, 6, 10, 15]  // 包含一个初始0

应用场景

1. 快速区间求和

给定区间[L, R]的和可以通过prefix[R+1] - prefix[L]快速计算:

  • 传统方法:遍历数组,时间复杂度O(n)
  • 前缀和方法:直接计算差值,时间复杂度O(1)

2. 解决子数组相关问题

如"和为k的子数组数量"、"最大子数组和"等问题:

  • 使用前缀和配合哈希表可以达到O(n)时间复杂度
  • 示例:求数组中和为k的连续子数组个数
    1. 计算前缀和数组
    2. 使用哈希表记录前缀和出现次数
    3. 遍历时检查当前前缀和 - k是否在哈希表中

3. 多维前缀和

可以扩展到二维甚至更高维度:

  • 二维前缀和用于快速计算矩形区域和
  • 构建方法:prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + arr[i][j]
  • 查询方法:通过四个角的prefix值相加减得到矩形和

复杂度分析

  • 预处理时间复杂度:O(n)
  • 空间复杂度:O(n)
  • 查询时间复杂度:O(1)

变种与应用示例

差分数组

前缀和的逆运算,用于高效处理区间更新:

  • 构建差分数组diff,其中diff[i] = arr[i] - arr[i-1]
  • 区间[L,R]增加valdiff[L] += val, diff[R+1] -= val
  • 通过差分数组的前缀和恢复原数组

1、前缀和

算法思想:

1、预处理出前缀和数组

dp[i]:1到i所有元素的和

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

2、使用前缀和数组

l,r\]所有元素和==dp\[l\]-dp\[r-1

细节问题:为什么要从1开始计数?

为了处理边界情况,添加虚拟结点。如果从0开始计数,想询问0到2算得是dp[2]-dp[-1].

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


int main() 
{
    //读入数据
    int n,q;
    cin>>n>>q;
    vector<int> arr(n+1);
    for(int i=1;i<=n;i++) cin>>arr[i];
    //预处理前缀和数组
    vector<long long> dp(n+1);//用long long防止溢出
    for(int i=1;i<=n;i++) dp[i]=dp[i-1]+arr[i];
    //使用前缀和数组
    int l=0,r=0;
    while(q--)
    {
        cin>>l>>r;
       cout<<dp[r]-dp[l-1]<<endl;     
    }
    return 0;

}
// 64 位输出请用 printf("%lld")

2、二维前缀和

算法思想:

1、预处理一个前缀和矩阵

dp[i,j]:表示从[1,1]到[i,j]所有元素的和

2、使用前缀和矩阵

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

int main()
{
    //读入数据
    int n=0,m=0,q=0;
    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]=dp[i-1][j]+arr[i][j]+dp[i][j-1]-dp[i-1][j-1];
    //计算处理前缀和矩阵
    int x1=0,x2=0,y1=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;
    }
    return 0;
}

3、寻找数组的中心下标

算法思想:

1、预处理前缀和和后缀和数组

前缀和:f[i] 0到i-1之间的和

f[i]=f[i-1]+nums[i-1]

后缀和:g[i] i+1到n-1之间的和

g[i]=g[i+1]+nums[i+1]

2、枚举0到n-1所有下标i,找到f[i]==g[i]

细节问题:初始化

要计算f(0)=0,g(0)=0

f从左向右,g从右向左

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

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

算法思想:

1、前缀积

前缀积:f[i] 0到i-1之间的积

f[i]=f[i-1]*nums[i-1]

后缀积:g[i] i+1到n-1之间的积

g[i]=g[i+1]*nums[i+1]

2、枚举0到n-1所有下标i,找到f[i]==g[i]

f从左向右,g从右向左

细节:f(0)=1, g(n-1)=1

cpp 复制代码
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;  
    }
};

5、和为k的子数组

算法思想:

细节问题:

1、前缀和加入哈希的时机

计算i的位置之前,只保存[0,i-1]的前缀和

2、不用真的创建前缀和数组

用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(auto&x:nums )
        {
            sum+=x;//计算当前位置前缀和
            if(hash.count(sum-k))  ret+=hash[sum-k];//统计个数
            hash[sum]++;
        }
        return ret;
    }
};

6、和可以被k整除的子数组(某一年蓝桥杯原题)

算法思想:前缀和+哈希表

在[0,i-1]找到右多少个前缀和的余数等于sum%k【(sum%k+k)%k】

cpp 复制代码
class Solution {
public:
    int subarraysDivByK(vector<int>& nums, int k) 
    {
        //前缀和的余数
        unordered_map<int,int> hash;
        hash[0%k]=1;//处理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;
    }
};

同余定理:

正式定义:

对于三个整数 a, b, m (其中 m > 0),如果 ab 除以 m 所得的余数相同 ,那么我们就说 ab 对模 m 同余。

记作:
a ≡ b (mod m)

假设有 a ≡ b (mod m)c ≡ d (mod m),那么:

性质 公式 C++ 代码操作提示
加法 a + c ≡ b + d (mod m) (a + c) % m 等价于 ( (a % m) + (c % m) ) % m
减法 a - c ≡ b - d (mod m) (a - c) % m 等价于 ( (a % m) - (c % m) + m) % m (注意加 m 是为了防止负数)
乘法 a * c ≡ b * d (mod m) (a * c) % m 等价于 ( (a % m) * (c % m) ) % m
幂运算 aⁿ ≡ bⁿ (mod m) 通过快速幂算法计算,基于乘法性质

⚠️ 重要警告:除法没有直接性质!
a / c ≡ b / d (mod m) 并不成立 !除法需要用到模逆元 的概念,这需要 cm 互质(gcd(c, m) == 1)。这是一个进阶话题,但非常重要。

数学概念 C++ 程序员的解读
a ≡ b (mod m) ab% m 操作下的结果是等价的
同余的性质 允许我们在计算过程中随时取模 ,就像拆括号一样 (a op b) % m = ((a % m) op (b % m)) % mop+, -, *
核心价值 将对大数的操作,转化为对一系列小余数的操作 ,从而避免整数溢出,使得一些原本不可能的计算成为可能。

C++/Java中负数%正数的结果及其修正:

C++ 中的取模运算

在 C++ 中,取模运算的结果符号与被除数 (a) 相同

表达式 结果 解释
7 % 3 1 正数取模,正常行为
-7 % 3 -1 结果符号与被除数相同
7 % -3 1 结果符号与被除数相同
-7 % -3 -1 结果符号与被除数相同

Java 中的取模运算

在 Java 中,取模运算的结果符号与除数 (b) 相同

表达式 结果 解释
7 % 3 1 正数取模,正常行为
-7 % 3 -1 结果符号与除数相同
7 % -3 1 结果符号与除数相同
-7 % -3 -1 结果符号与除数相同

数学上的期望行为

在数学中,我们通常期望取模运算的结果始终是非负的,并且在 [0, |b|-1] 范围内。例如:

  • -7 mod 3 应该等于 2(因为 -7 = (-3)*3 + 2

  • 7 mod -3 应该等于 1(因为 7 = (-2)*(-3) + 1

cpp 复制代码
// C++ 修正函数
int mod(int a, int b) {
    int r = a % b;
    // 如果余数为负,加上除数使其为正
    if (r < 0) {
        r += (b < 0) ? -b : b; // 确保加上的是正除数
    }
    return r;
}
java 复制代码
// Java 修正函数
public static int mod(int a, int b) {
    int r = a % b;
    // 如果余数为负,加上除数使其为正
    if (r < 0) {
        r += (b < 0) ? -b : b;
    }
    return r;
}

针对特定情况的简化修正

如果确定除数 b 是正数,可以使用更简单的修正:

cpp 复制代码
// 当 b > 0 时的简化修正
int mod_positive(int a, int b) {
    int r = a % b;
    return (r < 0) ? r + b : r;
}

使用标准库函数(C++17 及以上)

C++17 引入了 std::div 函数族,可以提供符合数学定义的除法结果

cpp 复制代码
#include <cstdlib>

std::div_t result = std::div(-7, 3);
int remainder = result.rem; // 结果为 2(符合数学定义)

7、连续数组

算法思想:前缀和+哈希表

1、将所有的0改为-1

2、在原数组中找到最长的子数组,使子数组中所有元素为0.

与之前的和为k的子数组类似

细节问题:

1、哈希表中一个存前缀和一个存下标

2、存入哈希表的时机:使用后丢入哈希表

3、如果有重复<sum,i>如何存?只保留前面的一对

4、默认前缀和为0的情况如何存储?hash[0]=-1

5、长度如何计算?如下图所示,i-j

cpp 复制代码
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;//将所有的0改成-1
            if(hash.count(sum))  ret=max(ret,i-hash[sum]);
            else hash[sum]=i;//去重
        }
        return ret;
    }
};

8、矩阵区域和

算法思想:

题目解析:

和之前的二维前缀和一样

1、预处理前缀和矩阵。dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i-1][j-1]

2、使用矩阵

ret[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]

1、求ans[i][j]

2、下标的映射关系

dp扩充一行一列来简化代码

cpp 复制代码
class Solution {
public:
    vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) 
    {
        int m=mat.size(),n=mat[0].size();
        //1、预处理前缀和矩阵
        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,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;
        
    }
};

本期内容到这里结束了喜欢请点个赞支持一下,谢谢。

相关推荐
Jooolin4 小时前
【C++】C++11出来之后,到目前为止官方都做了些什么更新?
数据结构·c++·ai编程
data myth4 小时前
洛谷p2392kkksc03考前临时抱佛脚 详解(回溯,深度搜索法)
算法
长沙红胖子Qt4 小时前
VTK开发笔记(三):熟悉VTK开发流程,编写球体,多半透明球体Demo
c++·qt
PPIO派欧云4 小时前
从套壳生态到上下文工程:一文看懂2025年Agent六大最新趋势
算法
Jooolin4 小时前
【C++】C++11都有什么新特性?
c++·ai编程·编程语言
Ka1Yan4 小时前
[算法] 双指针:本质是“分治思维“——从基础原理到实战的深度解析
java·开发语言·数据结构·算法·面试
freexyn4 小时前
Matlab自学笔记六十六:求解带参数的不等式
算法·matlab·参数方程·编程实例·解不等式
蓝风破云5 小时前
模拟实现STL中的list容器
c语言·数据结构·c++·链表·迭代器·list·iterator
DjangoJason5 小时前
每日算法题【二叉树】:二叉树的最大深度、翻转二叉树、平衡二叉树
数据结构·算法·链表