当前位置: 首页 > news >正文

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

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

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

目录

前缀和算法详解

基本概念

构建方法

应用场景

1. 快速区间求和

2. 解决子数组相关问题

3. 多维前缀和

复杂度分析

变种与应用示例

差分数组

1、前缀和

2、二维前缀和      

  3、寻找数组的中心下标      

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

5、和为k的子数组

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

        同余定理:

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

C++ 中的取模运算

Java 中的取模运算

数学上的期望行为

针对特定情况的简化修正

使用标准库函数(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,取决于边界处理)
原数组: [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] += valdiff[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].

#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、使用前缀和矩阵

#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从右向左

        

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

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标记前一个位置的前缀和

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】

        

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

        同余定理:

        正式定义:
        对于三个整数 abm (其中 m > 0),如果 a 和 b 除以 m 所得的余数相同,那么我们就说 a 和 b 对模 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) 并不成立!除法需要用到模逆元的概念,这需要 c 和 m 互质(gcd(c, m) == 1)。这是一个进阶话题,但非常重要。

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

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

C++ 中的取模运算

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

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

Java 中的取模运算

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

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

数学上的期望行为

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

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

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

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

针对特定情况的简化修正

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

// 当 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 函数族,可以提供符合数学定义的除法结果

#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

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改成-1if(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扩充一行一列来简化代码

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

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

http://www.dtcms.com/a/360448.html

相关文章:

  • 【Linux网络编程】应用层协议-----HTTP协议
  • PostgreSQL表膨胀的危害与解决方案
  • More Effective C++ 条款19:理解临时对象的来源(Understand the Origin of Temporary Objects)
  • centos 7 安装docker、docker-compose教程
  • AI 编程新玩法:用 yunqi-saas-kit 框架制作小游戏,看广告变现轻松赚钱​
  • 国产数据库之TiDB:博采众长
  • Ruoyi-vue-plus-5.x第二篇MyBatis-Plus数据持久层技术:2.2 分页与性能优化
  • [嵌入式embed]Keil5项目提示Missing: Compiler Version 5
  • 工业互联项目总结:UART
  • Backroom:信息代币化 AI 时代数据冗杂的解决方案
  • 漏洞基础与文件包含漏洞原理级分析
  • 使用 Python mlxtend库进行购物篮分析、关联规则
  • 软考中级习题与解答——第一章_数据结构与算法基础(3)
  • 进程状态 —— Linux内核(Kernel)
  • Linux 文件夹权限也会导致基本命令权限缺失问题
  • 【学Python自动化】 5. Python 数据结构学习笔记
  • postman带Token测试接口
  • 打工人日报#20250831
  • LangChain核心抽象:Runnable接口深度解析
  • * 和**有时展开,有时收集。*在对可迭代对象展开 **对字典展开。一般只看收集就够了,在函数定义的时候传入参数用
  • 第二十七天-ADC模数转换实验
  • linux系统学习(12.linux服务)
  • 【星闪】Hi2821 | SPI串行外设接口 + OLED显示屏驱动例程
  • 语音芯片3W输出唯创知音WTN6040FP、WT588F02BP-14S、WT588F04AP-14S
  • [回溯+堆优化]37. 解数独
  • Q1 Top IF 18.7 | 基于泛基因组揭示植物NLR进化
  • 高校心理教育辅导系统的设计与实现|基于SpringBoot高校心理教育辅导系统的设计与实现
  • 网格图--Day02--网格图DFS--面试题 16.19. 水域大小,LCS 03. 主题空间,463. 岛屿的周长
  • 技术总体方案设计思路
  • SAP报工与收货的区别(来自deepseek)