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

【算法思想】前缀和

目录

引入

基本介绍

例题1

题目描述:

分析1

同余原理

代码1

分析2

1. 初始化 cnt[0] = 1 的原因

2. 遍历时实时统计的逻辑

代码2:

例题2

问题描述:

思路1分析

思路1代码:

思路2分析: 

代码2

二维前缀和

问题1:

问题描述:

数据范围:

分析:

1.考虑最直观的暴力解法(❌)

2. 优化

3. 前缀和

二维前缀和(进阶前缀和)

4. 那我们了解过二位前缀和之后顺理成章可以在这个题里应用

1. 建立价值地图

步骤2. 计算二维前缀和

步骤3. 计算任意正方形区域的和

下面让我们——处理一些容易出错的地方,并给出代码

1. 处理坐标偏移

2. 计算前缀和

3. 遍历所有可能的正方形

代码:

总结

一、核心思想

二、典型应用场景

三、一些小tips

四、代码实现要点

五、最后总结


引入

输入一个长度为 n 的整数序列。

接下来再输入 m 个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

观察上面的要求,输入一个长度为n的整数序列,根据输入的询问,输出从第l个数到第r个数的和。

如果遍历后一个一个加,在数据很大的情况下,会超时。

那么怎么做才可以简化代码呢?

——用前缀和

基本介绍

前缀和,这三个字,顾名思义表示是一个和,并且是某段序列的前缀的和。

我们回到上一个题,如果我们提前知道第1个数到l-1个数的和,还有第1个数到第r个数的和,那么它们相减是不是可以直接得出结果,这个时间复杂度就是O(N)了;

那么如何得出从第一个数到某个数的和呢?(也就是前缀和的具体做法

先定义一个数组s[N],s[i]表示从第一个数字到第i个数字的和。

我们输入一段长度为n的序列,如果是一个个输入的,那么每输入一个就加到s[i]上,并且s[i]还需要加上s[i-1]

则代码为:

s[0]=0;
for(int i=1;i<=n;i++)
{
    cin>>a[i];
    s[i]=a[i]+s[i-1];
}

⭐前缀和有个很重要的部分: 由于s[i]需要去加上s[i-1]。为了使数组不访问到未知区域,所以s[i]中的i都需要从1开始,如果输入的a[i]输入必须要从0开始,那么代码就需要变成s[i+1]=a[i]+s[i]

🆗

现在基本了解了前缀和的思路,那么先做个例题来写一下吧

例题1

题目描述:

分析1

在题目中可看出来,它要求计算Ai到Aj的和是否是K倍的,

那根据上面介绍的前缀和思想,我们很容易能想出做题方法。

首先定义两个数组长度为N的最大值(100010)多一个10,这样比较保险。

一个数组是用于存储长度为n的数组a[N],一个存储前缀和s[N].

⭐注意,我们定义完后,需要明晰前缀和数组s[i]表示的是什么意思——s[i]表示从1开始到第i个元素的总和。

求出s[i]数组后,我们现在需要解决输出问题——我们需要输出区间内的和为K倍的区间数量。

很容易的能想到for循环嵌套,去一一遍历,但由于数据范围是10的5次方,n的平方的时间复杂度显然是过不去的。所以需要另寻方法

这里引入一个数学上的(同余原理

同余原理

如果想让(s[i]-s[j-1]被k整除,等同于,s[i]取余k等于s[j-1]取余k。

那么为了方便,我们在输入a[i]数组,给s[i]赋值的同时,直接取余k,

那这时候可能会有人疑惑了,取余k之后,去判断前缀和数组的两个数是否相等不还是需要两个for循环嵌套吗

这里就需要借用另一个数组来实现了,我们定义一个st[N]数组,

它表示  s数组中取余k后结果相同的  数量

表示形式为——st[s[i]],这个就可以表示与s[i]取余k相等的数量

⭐注意,1个数也可以当数组,所以不用去掉s[i]本身的数量

而取余0也就是st[0]表示:前缀和取模后余数为 0 的次数。每个余数为 0 的前缀和本身对应一个 从起点到当前位置的区间,这些区间的和本身就是 K 的倍数。

而且st[s[i]],它最终要算的次数是从这个结果里面,拿出两个数作为区间端点,也就是C(m,2),从m个数任意挑选两个数字,表示为:(st[i]*(st[i]-1))/2

分析完了,现在可以写出代码了。

代码1

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
#define N 100010

long long s[N],st[N];
int a;
int flag;

int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a);
        s[i]=(s[i-1]+a)%k;
        st[s[i]]++;
    }
    
    long long res=st[0];
    
    for(int i=0;i<k;i++)//取余结果一定不大于k
    {
        res+=st[i] * (st[i] - 1) / 2;
    }
    
    cout<<res<<endl;
    return 0;
}

代码做了一些优化,把数组变为了一个int变量,输入这个数的时候同时计算s[i]和st[i]的值,注意数值范围数组是long long 。

分析2

或者可以借用另一种方法,把st[0]设置为1,然后每次算完s[i]之后,就加上st[s[i]],再让st[s[i]]加加。

这个思路是怎么想的呢?

利用前缀和的同余性质,动态维护余数出现次数,并实时统计所有可能的有效区间

1. 初始化 cnt[0] = 1 的原因
  • 虚拟前缀和:引入一个不存在的 s[0] = 0(对应空数组的和)。

    • 作用:当某个实际前缀和 s[i] ≡ 0 (mod K) 时,区间 [1, i] 的和自动满足 s[i] - s[0] ≡ 0 (mod K),即该区间有效。
    • 示例:若 A = [3]K = 3,则 s[1] = 0,此时 s[1] - s[0] = 0,区间 [1,1] 有效。
  • 避免漏算:如果没有这个虚拟的 s[0],当 s[i] ≡ 0 时,无法统计从数组开头到 i 的区间。

2. 遍历时实时统计的逻辑
  • 操作步骤:
    对每个 i(从 1 到 N):

    1. 计算 s[i] = (s[i-1] + A[i]) % K
    2. 立即将 cnt[s[i]] 累加到结果:
  • 当前 s[i] 的余数为 r,之前所有余数为 r 的前缀和(即 cnt[r] 次)均可与当前 s[i] 形成有效区间。
    3. 再更新 cnt[s[i]]:将当前余数 r 的计数加 1,供后续前缀和匹配。

  • 动态维护的直观解释:

    • 统计的是历史匹配次数:每次处理 s[i] 时,cnt[r] 表示在 i 之前,余数为 r 的前缀和已经出现的次数。
    • 组合数的实时计算:每个新余数 r 贡献 cnt[r] 个新区间,最终所有余数 r 的总贡献为 Σ cnt[r] * (cnt[r]-1)/2(但通过动态累加避免了显式计算组合数)。

假如s[1]是1,而s[2]也是1,第一次的时候由于s[1]无法和别人组成区间,所以res+=0;而当到s[2]时,由于此时cnt[1]加加过了,所以为1,res+=1;表示1,2组成一个区间。

也就是位置2到位置2的和属于是K倍区间的情况。

代码2:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
#define N 100010

long long s[N],st[N];
int a;
int flag;

int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    
    st[0]=1;//这样从头开始到第i个位置的这种情况就不会被漏掉。
    //它表示s[i]=0,也就是这个和本身就是K倍。
    
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a);
        s[i]=(s[i-1]+a)%k;
        
        res+=st[s[i]];
        st[s[i]]++;
    }
    
    cout<<res<<endl;
    return 0;
}

例题2

问题描述:

思路1分析

分析一下这个题目,有三个数组A、B、C,他们长度为N,问有多少三人组,是第一个数组A是最小的,第二个数组B大小属于中间,第三个数组C是最大的。

很好想到的思路是遍历三个数组,嵌套3个for循环,但是我们看一下数据范围:

显而易见超时了,10的5次方只允许有一次for循环,那么如何优化才可以不超时输出正确结果呢?

既然只能支持一次for循环,那么如果只能挑选一个数组去循环的话,选哪个呢?

选A和C的话,把最大或者最小的先定下来,但是另外两个仍然属于不固定的状态

那么遍历B,遍历这个大小处于中间的值,从A中选择大小小于B的,从C中选择大小大于C的,

显而易见可以用二分,先排序好数组A和C,然后遍历B,利用二分求出A中小于B的,以及C中大于B的。A中小于B的乘以C中大于B的,再把所有结果相加即可。

既然按照这个思路分析完了,那么下面就是展示代码了:

思路1代码:

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=10e5+10;
int a[N],b[N],c[N];
int n;

int main()
{
    scanf("%d",&n);
    
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=0;i<n;i++)
    {
        scanf("%d",&b[i]);
    }
    for(int i=0;i<n;i++)
    {
        scanf("%d",&c[i]);
    }
    
    sort(a,a+n);
    sort(c,c+n);
    
    long long res=0;
    
    for(int i=0;i<n;i++)
    {
        int l=-1;int r=n;
        while((l+1)!=r)
        {
            int mid=l+r>>1;
            if(a[mid]<b[i])l=mid;
            else r=mid;
        }int p=l+1;//l从0开始的
        
        
        l=-1; r=n;
        while((l+1)!=r)
        {
            int mid=l+r>>1;
            if(c[mid]<=b[i])l=mid;
            else r=mid;
        }int q=n-l-1;
        
        res+=(long long)p*q;
        //cout<<p<<" "<<q<<endl;
    }
    
    cout<<res<<endl;
    
    return 0;
}

思路2分析: 

那么有什么更简便的方法吗?

YES,回归正题,这题可以用前缀和。

注意:前缀和只是个思想,不要把它禁锢在只能计算数组里的一段数字的和的情况。

这个题的前缀和就不是很好想了,那么开始分析一下该如何优化吧

根据之前的思路,已知B[j],我们需要求出小于B[j]的所有A[i]和C[i],假设我们用一个数组存储小于B[j]的数量,但是要提前将数组A中所有元素出现的次数存入一个哈希表中,

最后即可以快速查找小于B[i]的所有元素的总数——只需要在枚举之前先将求出这个哈希表中各数的前缀和。

显而易见,需要两个数组,(对于A而言)

那么一共需要四个数组。

cnt[N]和s[N],

cnt[i]代表等于i的数有多少个?

s[i]表示从等于1到i的总共数量。——》如果所以如果要求小于B[i]的数量,只需要知道s[i-1]即可。

理论成立,下面开始实施

代码2

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=10e5+10;
int a[N],b[N],c[N];
int n;

long long cnt1[N],cnt2[N];
long long s1[N],s2[N];

void sr(int *p,long long *cnt)
{
    for(int i=0;i<n;i++)
    {
        scanf("%d",&p[i]);
        p[i]++;
        cnt[p[i]]++;//由于p[i]从0开始计数,而且有i-1的情况,所以为了不存在-1越界数组的情况,范围变成1到10的5次方+1
    }
}

int main()
{
    scanf("%d",&n);
    
    sr(a,cnt1);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&b[i]);
        b[i]++;
    }
    sr(c,cnt2);
    
    long long res=0;
    
    for(int i=1;i<=N;i++)
    {
        s1[i]=cnt1[i]+s1[i-1];//cnt表示等于i的数的次数,s表示从1到i,等于其中任何一个数的次数和
        s2[i]=cnt2[i]+s2[i-1];
    }
    
    for(int i=0;i<n;i++)
    {
        res+=s1[b[i]-1]*(s2[N]-s2[b[i]]);
    }
    
    cout<<res<<endl;
    
    return 0;
}

其中体现了一个非常重要的有关前缀和的东西——当数组元素的大小从0开始时,如果需要数组元素作为下标的话,需要偏移——》因为前缀和需要使用加上s[i-1],如果从0开始,就需要访问越界部分。

当然记得更改所有的相关数组。如果你在这个代码里忘记b[i]++,那么会无法得出正确答案的。 

二维前缀和

刚才的所有问题都是一维层面的,现在需要去解决二维方面的前缀和

问题1:

问题描述:

数据范围:

分析:

我们需要找出一个 R×R的正方形区域,这个区域的边必须和坐标轴平行,使得这个区域内所有目标点的价值总和最大。目标点可能有多个出现在同一坐标位置。


1.考虑最直观的暴力解法(❌)

看完题目后很可能会想到这样的解法:

  1. 首先遍历地图上的每个可能位置作为正方形的左上角

  2. 对于每个位置,计算这个正方形内所有点的价值之和

  3. 记录最大值

按照最大值来计算——假设地图范围是5000×5000,正方形边长是R=5000。那么需要计算的次数是:

  • 外层循环次数:(5000 - R + 1) × (5000 - R + 1) ≈ 2500万次

  • 内层每次计算需要遍历R×R次(假设R=5000,就是2500万次)
    总计算量达到 2500万 × 2500万 = 62.5万亿次!显而易见超时了。


2. 优化

我们需要找到一种方法,能够 快速计算任意矩形区域的和,而不是每次都重新遍历去暴力求解。这时,"前缀和"的概念就派上用场了。


3. 前缀和

说到前缀和,我们来回顾一下一维前缀和的一般使用方法

  • 一般情况下, S[i]通常表示前i个元素的和(从第1个到第i个),但是就如上面的第二题一样,不一定是这个含义,要根据题目意思来分析。

  • 计算区间[a,b]的和:S[b] - S[a-1]


二维前缀和(进阶前缀和)

现在问题扩展到二维。想象一个网格地图,每个格子有一个价值:

目标:快速计算任意矩形区域的和。例如下图中红色区域的和:

定义二维前缀和

  • S[i][j] 表示从左上角(1,1)(i,j)形成的矩形区域的总和

计算区域和公式
要计算区域[x1,y1][x2,y2]的和:

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

(⭐记得最后要加上被减了两次的左上角区域)


4. 那我们了解过二位前缀和之后顺理成章可以在这个题里应用

1. 建立价值地图
  1. 创建一个二维数组g[][],其中g[x][y]表示坐标(x,y)处所有目标点的价值总和(可能有多个目标点在此处)

  2. 将输入的坐标转换为数组索引(注意题目中坐标从0开始,但通常处理时会让数组索引从1开始,避免边界问题)

示例
假设输入三个目标点.

(0,0) 价值5
(1,1) 价值3
(1,1) 价值2

则:

  • g[1][1] = 5(坐标(0,0) → 数组索引(1,1))

  • g[2][2] = 3+2=5(坐标(1,1) → 数组索引(2,2))


步骤2. 计算二维前缀和

根据定义,计算每个S[i][j]

S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + g[i][j]

(当前格子的前缀和 = 上方前缀和 + 左方前缀和 - 左上角前缀和 + 当前价值)


步骤3. 计算任意正方形区域的和

假设炸弹覆盖的右下角在(i,j),边长为R,则这个正方形的左上角是(i-R+1, j-R+1)。区域和为:

sum = S[i][j] - S[i-R][j] - S[i][j-R] + S[i-R][j-R]

示例
假设R=2,计算以(3,3)为右下角的2×2区域和:

sum = S[3][3] - S[1][3] - S[3][1] + S[1][1]

下面让我们——处理一些容易出错的地方,并给出代码

1. 处理坐标偏移
int x,y,w;
cin >> x >> y >> w;
x++; y++; // 将坐标转换为1-based索引
g[x][y] += w; // 累加同一位置的价值
  • 原题坐标范围是0~5000 → 转换为1~5001,避免处理负数索引

2. 计算前缀和
for(int i=1; i<=a; i++)
    for(int j=1; j<=b; j++)
        s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
  • 这里s数组既存储原始价值,又存储前缀和(节省内存)

3. 遍历所有可能的正方形
for(int i=r; i<=a; i++)
    for(int j=r; j<=b; j++)
        res = max(res, s[i][j] - s[i-r][j] - s[i][j-r] + s[i-r][j-r]);
  • 枚举正方形的右下角坐标(i,j)

  • 通过前缀和公式快速计算区域和

代码:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=5010;

int a,b;
int s[N][N];//原数组,后面可以变成二维数组存储前缀和


int main()
{
    int n,r;
    cin>>n>>r;
    r=min(5001,r);
    
    a=b=r;
    while(n--)
    {
        int x,y,w;
        cin>>x>>y>>w;
        x++;
        y++;
        a=max(a,x);
        b=max(b,y);
        s[x][y]+=w;
    }
    
    
    for(int i=1;i<=a;i++)
    {
        for(int j=1;j<=b;j++)
        {
            s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
            //cout<<s[i][j]<<endl;
        }
    }
    
    int res=0;
    //枚举它,从右下角枚举所以初始坐标为r
    for (int i = r; i <= a; i ++ ){
        for (int j = r; j <= b; j ++ ){
            res = max(res, s[i][j] - s[i - r][j] - s[i][j - r] + s[i - r][j - r]);
    
        }
    }
    
    cout<<res<<endl;
    return 0;
}

 

总结

前缀和思想是一种通过预处理数组来高效计算区间和的算法技巧,其核心在于通过预计算并存储前n项的和,将原本复杂的区间查询问题转化为简单的差值运算,从而大幅优化时间复杂度。以下是其详细用法总结:

一、核心思想

  1. 预处理数组:构建前缀和数组 sum,其中 sum[i] 表示原数组前i项的和。例如,原数组 a[1...n] 的前缀和数组满足 sum[i] = sum[i-1] + a[i],从而快速计算任意区间 [l, r] 的和为 sum[r] - sum[l-1] 。
  2. 优化时间复杂度:将暴力法的O(n²)或O(n³)复杂度降至O(n)预处理 + O(1)查询,适用于多次区间和查询的场景 。

二、典型应用场景

  1. 一维数组问题

    • 寻找中心索引:通过比较左侧前缀和与右侧(总和 - 左侧 - 当前元素)是否相等,快速定位中心索引 。
    • 和为K的子数组:利用哈希表记录前缀和出现的次数,统计满足 sum[j] - sum[i] = K 的子数组数量,将时间复杂度从O(n²)优化至O(n) 。
    • 统计特定条件的子数组:如优美子数组(含k个奇数)、和可被K整除的子数组,结合哈希表记录前缀奇数的数量或余数分布,实现高效统计 。
  2. 二维矩阵问题

    • 子矩阵和计算:构建二维前缀和矩阵 s[i][j],通过公式 s[i][j] = prefixSum[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j] 预处理,查询时用容斥原理计算子矩阵和 。

三、一些小tips

  1. 结合哈希表:在统计满足特定条件的子数组时,哈希表可存储前缀和(或余数、奇数个数等)的频率,避免双重循环遍历。例如,哈希表初始化需包含 hash[0] = 1,以处理前缀和直接等于目标值的情况 。
  2. 负数处理:当涉及余数计算(如和可被K整除)时,需将负数余数调整为正值。例如,(sum % K + K) % K 确保余数在 [0, K-1] 范围内 。
  3. 空间优化:无需显式构建前缀和数组时,可直接用变量累加前缀和,减少空间占用 。
  4. 扩展应用:前缀和思想可推广至其他运算(如异或、乘积)或变种问题(如前缀GCD、差分数组),例如统计区间异或结果或动态数组修改后的区间和 。

四、代码实现要点

  • 一维前缀和模板
  
  for (int i=1; i<=n; ++i) {
      sum[i] = sum[i-1] + arr[i];
  }
  // 查询区间[l, r]的和:sum[r] - sum[l-1]
  • 二维前缀和模板

 for (int i=1; i<=n; ++i) {
      for (int j=1; j<=m; ++j) {
          s[i][j] = s[i-1][j] + s[i][j-1] 
                          - s[i-1][j-1] + a[i][j];
      }
  }

五、最后总结

前缀和思想——关键在于灵活运用预处理数据与哈希映射,同时注意边界条件与负数处理,可以用在不同的算法题场景中。

相关文章:

  • 前端Html5 Canvas面试题及参考答案
  • Harbor 高可用部署
  • 【RH124】第一章 红帽企业Linux入门
  • 李白打酒加强版--dfs+记忆化搜索
  • Cursor插件市场打不开解决
  • JMX 和 JAAS 认证
  • 【数据结构】栈和队列
  • 【NLP】 9. 处理创造性词汇 词组特征(Creative Words Features Model), 词袋模型处理未知词,模型得分
  • 3.4 基于TSX的渲染函数类型安全实践
  • Java中的I/O
  • Hive函数大全:从核心内置函数到自定义UDF实战指南(附详细案例与总结)
  • Python中的unittest库
  • Java 并发编程——BIO NIO AIO 概念
  • C语言:基于数组实现栈
  • 如何打包数据库mysql数据,并上传到虚拟机上进行部署?
  • pandas表格内容比较
  • 数据链路层协议
  • 共享内存通信效率碾压管道?System V IPC原理与性能实测
  • 求和23年蓝桥杯省赛
  • go程序运行Spaitalite踩坑记录
  • 雅典卫城上空现“巨鞋”形状无人机群,希腊下令彻查
  • 媒体评欧阳娜娜遭民进党当局威胁:艺人表达国家认同是民族大义
  • 独家 |《苏州河》上海上演,编剧海飞:上海的风能吹透我
  • 九江宜春领导干部任前公示,3人拟提名为县(市、区)长候选人
  • 董军在第六届联合国维和部长级会议上作大会发言
  • 把中国声音带向世界,DG和Blue Note落户中国