前缀和的学习及刷题

前缀和

1.前缀和的定义

  • **前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分**可以看成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。

    (数学角度上分析就是说,给定一数列an,其前n项和为Sn,那么Sn构成的数列是an数列的前缀和数列;而反过来,an数列被称为是Sn数列的差分数列。这种关系就像是导数和不定积分一样,求导后的函数是原函数的导数;原函数是求导后函数的不定积分。)

2.怎么利用前缀和

  • 给出一个问题:

    输入一个长度为n的整数序列。接下来再输入m个询问,每个询问输入一对l, r。对于每个询问,输出原序列中从第l个数到第r个数的和。

  • 法一:暴力解法

    先输入长度为n的数组,对数组进行遍历,每一次遍历找出对应的位置元素进行相加,遍历m次

    代码如下:

    #include <iostream>
    using namespace std;
    
    int main()
    {
        int n,m;cin>>n>>m;
        int a[100010];
        for(int i =1;i<=n;i++)
        {
            cin<<a[i];
        }
        while(m--)
        {
            int l,r,sum=0;cin>>l>>r;
            for(int i=l;i<=r;i++)
            {
                sum+=a[i];
            }
            cout<<sum;
        }
    }
    

    (这样的时间复杂度为O(n * m),如果n和m的数据量稍微大一点就有可能超时,而我们如果使用前缀和的方法来做的话就能够将时间复杂度降到O(n + m),大大提高了运算效率。)

  • 法二:前缀和法

    先创建一个数组S[n]用于存放前缀和,接着在输入数据an时对数据进行处理,即求出对应的Sn,放进数组S[n]中;紧接着就是输入l和r,每输入一对l,r就求出S[r+1]-S[l]的值,所得值就是所求的区间值

    代码如下:

    #include <iostream>
    using namespace std;
    
    int main()
    {
        int S[100010];
        int n,m;cin>>n>>m;
        for(int i =1;i<=n;i++)
        {
            int a;cin>>a;
            S[i]=S[i-1]+a;
        }
        while(m--)
        {
            cout<<S[r]-S[l-1]<<endl;
        }
        return 0;
    }
    

    (很明显啊,该方法的空间复杂度较法一小,空间复杂度为O(n+m),大大提高了运算效率。前缀和的应用主要是为了提高求解静态数组区间和的效率。)

    image-20230109150149602

3.一维前缀和

  • 一维前缀和,顾名思义就是一维数组的前缀和,即一维数组中前n个元素的和。
  • 规律:
    • 一维数组an的某一前缀和:Sn=Sn-1+an
    • 一维数组an某一区间[l, r]的和:=S[r]-S[l-1]
  • 总结:image-20230109151551604

4.二维前缀和

  • 定义:

    给定一个二维数组a[] [],s[i] [j]表示二维数组中,左上角(1, 1)到右下角(i, j)所包围的矩阵元素的和(s[i] [j]所代表的值并不是从[1] [1]到[i] [j]的所有元素之和。)

  • 求二维数组某区间的前缀和;

    (x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
    s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]

  • 规律:

    • 二维数组**某一前缀和**:s[i, j]=s[i, j-1]+s[i-1, j]-s[i-1, j-1]+a[i,j]
    • 二维数组**某一区间和**:=s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]`

    image-20230109153740250

  • 例题:

    输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。

    对于每个询问输出子矩阵中所有数的和。

    输入格式
    第一行包含三个整数n,m,q。

    接下来n行,每行包含m个整数,表示整数矩阵。

    接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。

    输出格式

    共q行,每行输出一个询问的结果。

    思路:先创建一个数组用于储存二维数组an的数据;接着将数据输入到数组s[N] [N];再利用二维数组前缀和的公式s[i, j]=s[i, j-1]+s[i-1, j]-s[i-1, j-1]+a[i,j]对数组s进行遍历(对an数组进行遍历并将Sn的数据覆盖,是为了方便加上a[i, j],将原来数组s[N] [N]的数据覆盖,获得Sn数组。

    代码如下:

    #include <iostream>
    using namespace std;
    const int N = 1010;
    int n, m, q;
    int s[N][N];
    int main()
    {
        scanf("%d%d%d", &n, &m, &q);
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                scanf("%d", &s[i][j]);
        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];
        while (q -- )
        {
            int x1, y1, x2, y2;
            scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
            printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
        }
        return 0;
    }
    
    

5.前缀和应用场景及模版

  • 应用场景

    • 某个静态数组某个**区间所有元素求和**

    • 统计区间和是k的倍数的区间总共有多少个

  • 模版

    一维前缀和

    • int a[N], s[N];
      
      //构造一维前缀和
      for(int i = 1; i <= n; i++)
      {
          s[i] = s[i-1] + a[i];
      }
      
      //求[i, j]区间的元素总和
      int sum = s[j] - s[i-1];
      

    二维前缀和

    • int a[N]{N], s[N][N];
      
      //构造二维前缀和
      for(int i = 1; i <= n; i++)
      {
          for(int j = 1; j <= n; j++)
          {
              s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
          }
      }
      
      //求(x1,y1)到(x2,y2)区间的元素总和
      int sum = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
      

6.例题

【Acwing 795.前缀和】

  • 分析

    • 根据题目意思就是要求返回一维数组的某个区间和。
    • 这道题就是经典的一维前缀和问题,已知 an 数组中的每一项,我们便可以通过数列的前 n 项和公式 Sn = Sn-1 + an 构造前缀和数组 Sn 。构造好前缀和数组后,若要求 [l, r] 区间内的元素和,直接 Sr - Sl-1 即可。
  • 设计思路

    • 定义一个数组 a[N] 用于存储静态数组的数据,一边输入数据的同时通过 S[i] = S[i-1] +a[i] 构造数组 a[N] 的前缀和数组。
    • 通过输入的 lr ,利用公式 S[r] - S[l-1] 计算出区间元素和。
  • 代码

    • #include <iostream>
      using namespace std;
      
      const int N = 100010;
      int n, m;
      int a[N], s[N];
      
      int main()
      {
          cin >> n >> m;
          for(int i = 1; i <= n; i++)
          {
              scanf("%d", &a[i]);
              s[i] = s[i-1] + a[i]
          }
          
          while(m -- )
          {
              int l, r;
              scanf("%d%d", &l, &r);
              printf("%d\n", s[r] - s[l-1]);
          }
          return 0;
      }
      

【Acwing 796.子矩阵的和】

  • 分析

    • 题目意思就是要求返回二维数组的某个区间和。
    • 这是一个典型的二维前缀和问题。通过输入的二维数组 a[N][N] ,利用二维前缀和递推式,构造出二维前缀和数组 s[N][N]
    • [x1, y1]到[x2, y2]区间所有元素和:s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]
  • 设计思路

    • 定义一个数组 a[N][N] 用于存储静态数组的数据,一边输入数据的同时通过递推式 s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j] 构造数组 a[i][j] 的前缀和数组。
    • 通过输入的 x1,y1,x2,y2 ,利用公式 s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] 计算出区间元素和。
  • 代码

    • #include <iostream>
      using namespace std;
      
      const int N = 1010;
      int n, m, q;
      int a[N][N], s[N][N];
      
      int main()
      {
          scanf("%d%d%d", &n, &m, &q);
          for(int i = 1; i <= n; i ++ )
          	for(int j = 1; j <= m; j ++ )
              {
                  scanf("%d", &a[i][j]);
                  s[i][j] = s[i][j-1] + s[i-1][j] - s[i-1][j-1] + a[i][j];
              }
          
          while( q -- )
          {
              int x1, y1, x2, y2;
              scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
              printf("%d\n", s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
          }
          return 0;
      }
      

【Acwing 99.激光炸弹】

  • 分析

    • 根据题意,要求返回激光炸弹能炸掉地图上的最大总价值。

    • 每个激光炸弹可以摧毁包含在 RxR 区域内的目标价值(不含边界上的点)。换而言之,题目要求的是所有 RxR 区间的目标和最大值,一个区间的目标和有不同的取值,如下图所示:

      在这里插入图片描述

    • 由上图可知,一个区间的目标和最大时,应该是上图2所示的炸法,即取 RxR 个点位。求区间和,就可以用典型的**二维前缀和**。

    • 对于每个 RxR 个点位总和进行遍历,统计出最大值即可。

    • 时间复杂度分析:地图总的目标点位个数为 5001x5001 ,构造前缀和数组所用的最大时间为 5001x5001 ,遍历 RxR 区间的最大次数为 5001 ,因此所花的总时间为 5001x5001+5001 << 1e9 ,不存在超时的问题。

    • 空间复杂度分析:地图上总的点位数为 5001x5001 ,我们定义原数组为 int 类型 a[5001][5001] ,前缀和数组为 int 类型 s[5001][5001] ,所申请的总的内存空间为 5001x5001x4/(1024x1024) = 200MB,很显然,存在爆内存的问题,因此我们只能开一数组 s[N][N] 同时用作原数组和前缀和数组。 (1MB = 1024x1024Byte

  • 设计思路

    • 先定义一个二维数组 s[N][N] 用于存储原数组和前缀和数组。定义两个变量 ,n 表示地图的宽度,m 表示地图的长度。
    • 接着定义一个变量 r 表示激光炸弹的爆炸范围,并求r = min(r, 5001) ,用 rn,m 进行初始化。
    • 然后循环输入目标的坐标和价值,同时更新 n,m 的值。
    • 利用二维前缀和递推式 s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j] 构造前缀和数组。
    • 最后遍历每个 rxr 区间和,统计出价值最大的那个数值返回即可。
  • 代码

    • #include <iostream>
      #include <cstring>
      #include <algorithm>
      using namespace std;
      
      const int N = 5010;
      
      int n, m;//n表示x坐标最大值,m表示y坐标最大值
      int s[N][N];//该数组既是原数组,又是前缀和数组
      
      int main()
      {
          int cnt, r;
          cin >> cnt >> r;
          
          r = min(5001, r);
          
          n = r;
          m = r;
          
          for(int i = 0; i < cnt; i++)
          {
              int x, y, w;
              scanf("%d%d%d", &x, &y, &w);
              x ++; y++; //横纵坐标都加一是为了,在预处理前缀和不用考虑边界问题,横纵坐标均大于等于1
              s[x][y] += w;
              n = max(x, n);
              m = max(y, m);
          }
          
          //构造前缀和数组
          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] + s[i][j];
              }
          }
          
          int res = 0;
          
          for(int i = r; i <= n; i++)
          {
              for(int j = r; j <= m; 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;
      }
      

【Acwing 1230.K倍区间】

  • 分析

    • 根据题意,从数列的第一位查询区间 [i,j] 的元素和是否是 k 的倍数,统计此类区间的个数。
    • 首先我们从最暴力的方法做起,先从 0到n 枚举 i,接着从 i到n 枚举 j ,然后再从 i~j 统计总和,最后特判一下是否是k的倍数。总的时间复杂度是 O(n^3) ,根据题目要求显然 n1e6 的时候肯定超时。
    • 对于区间和问题,我们可以采用前缀和的方法来空间换时间,在计算 i~j 的总和上,我们采用前缀和求解,总的时间复杂度为 O(n^2),但显然这样还是不行,还是会出现超时的情况。
    • 我们对题目的条件在前缀和思路下,再细细分析一下,看能不能再优化一层循环。已知前缀和数组 s[N] ,区间和 sum = s[j] - s[i-1],要判断区间是否是 k 倍区间,只需判断是否满足 s[j] - s[i-1] % k == 0该判断式子也可以变换成 s[j] % k == s[i-1] % k
    • 对于以上式子我们不难发现一个思路:由于 i-1 一定比 j 小,所以在遍历 s[j] % k 的大小时,s[i-1] % k 的大小已经遍历过了。因此我们只需要在遍历 s[j] 的同时,用数组 cnt[s[j]%k]++ 记录在 j 之前余数等于 s[j]%ks[i] 的个数,每一次遍历 s[j]res += cnt[s[j]%k],即可统计出所有的 k 倍区间的个数。
  • 设计思路

    • 首先定义一个long long 类型的数组 s[N] 用于存储前缀和,另外定义一个数组 cnt[N] 用于存储余数相同的 s[i] 的个数。
    • 通过输入的元素构造前缀和数组,并且定义一个变量 res 用于存储最终统计的 k 倍区间个数。
    • 接着遍历 s[j] 更新 res 的同时,更新 cnt[s[j]] 的大小。
    • 最后输出 res + cnt[0] 。(原因是每次更新 res 都是 res += cnt[s[j]%k]res 加的 cnt[s[j]%k] 表示在 j 之前有多少个 s[i]k 求余等于 s[j]%k ,不包括当前 s[j] 。对于 s[j]%k == 0 ,res还要加上 s[j] 本身,因此最终答案是 res + cnt[0]
  • 代码

    • #include <iostream>
      using namespace std;
      
      const int N = 100010;
      long long s[N], cnt[N];
      int n, k;
      
      int main()
      {
          scanf("%d %d", &n, &k);
          for(int i = 0; i <= n; i ++)
          {
              scanf("%lld", &s[i]);
              s[i] += s[i-1];
          }
          
          long long res = 0;
          
          for(int i = 0; i <= n; i++)
          {
              ans += cnt[s[i]%k];
              cnt[s[i]%k]++;
          }
          printf("%lld", (long long)res+cnt[0]);
          return 0;
      }
      
  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值