/** 鼠标样式 **/

Shu-How Zの小窝

Loading...

前缀和与差分

第一讲 前缀和与差分

听讲人:24级算法组小萌新

卑微讲题人:22049212-梁军斌(本人正在疯狂整理入党材料中)

我认为,每个人最重要的不是过去,而是现在;虽然背负着昨日的重担,但也有自己想要追寻的东西。
希望你从今以后无论去往何处都不要忘记,世界上有那么多爱你的人。
你道你机关算尽,可你真算到了自己也要下场当棋子吗?
坐看日月行,细数千帆过。
年年今日,灯明如昼。原火不灭,愿人依旧。

先来看一道例题

前缀和

题目描述

给定一个长度为 n 的整数序列。接下来输入 m 个询问,每个询问由一对整数 l,r 组成。

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

输入格式

  • 第一行包含两个整数 nm
  • 第二行包含 n 个整数,表示整数数列。
  • 接下来 m 行,每行包含两个整数 lr,表示一个询问的区间范围。

输出格式

  • m 行,每行输出一个询问的结果。

数据范围

  • 1lrn
  • 1n,m100000
  • 1000 数列中元素的值 1000

示例

输入样例

5 3
2 1 3 6 4
1 2
1 3
2 4

输出样例

3
6
10

参考代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
using i64 = long long;
const int N = 3e5+10;
void solve()
{
    int n,m;
    cin>>n>>m;
    vector<int> a(n+1,0);
    for(int i=1;i<=n;i++) cin>>a[i];
    vector<int> s(n+1,0);
    for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
    while(m--)
    {
        int l,r;
        cin>>l>>r;
        cout<<s[r]-s[l-1]<<endl;
    }
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _=1;
    //cin>>_;
    while(_--)
    {
        solve();
    }
    
    
}

前缀和

定义

前缀和可以简单理解为「数列的前 n 项的和」,是一种重要的预处理方式,能大大降低查询的时间复杂度。

C++ 标准库中实现了前缀和函数 std::partial_sum,定义于头文件 <numeric> 中。

设数列的前x项的和为Sx

  • 已知一个长度为n的数组a:

  • S1 = a1

  • S2 = a1 + a2

  • S3 = a1 + a2 + a3

  • ...

  • Sn1 = a1 + a2 + a3 + ... + an1

  • Sn = a1 + a2 + a3 + ... + an1 + an

知道前缀和,便可以知道任意区间内所有数之和,即Suml,r = Sr - Sl1:

  • Sl1 = a1 + a2 + a3 + ... + al1
  • Sr = a1 + a2 + a3 + ... + ar1 + ar
  • Suml,r = al + al+1 + al+2 + ... + ar = Sr - Sl1;

例:

  • Sum3,9 = S9 - S2
  • Sum3,7 = S7 - S2
  • Sum3,3 = S3 - S2

如何求前缀和?

首先初始化a[0]=0,随后遍历一遍数组a,得到前缀和数组

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

二维前缀和

基于容斥原理

这种方法多用于二维前缀和的情形。给定大小为 m×n 的二维数组 A,要求出其前缀和 S。那么,S 同样是大小为 m×n 的二维数组,且

Si,j=iijjAi,j.

类比一维的情形,Si,j 应该可以基于 Si1,jSi,j1 计算,从而避免重复计算前面若干项的和。但是,如果直接将 Si1,jSi,j1 相加,再加上 Ai,j,会导致重复计算 Si1,j1 这一重叠部分的前缀和,所以还需要再将这部分减掉。这就是 容斥原理。由此得到如下递推关系:

Si,j=Ai,j+Si1,j+Si,j1Si1,j1.

实现时,直接遍历 (i,j) 求和即可。

考虑一个具体的例子。

二位前缀和示例

这里,S 是给定矩阵 A 的前缀和。根据定义,S3,3 是左图中虚线方框中的子矩阵的和。这里,S3,2 是蓝色子矩阵的和,S2,3 是红色子矩阵的和,它们重叠部分的和是 S2,2。由此可见,如果直接相加 S3,2S2,3,会重复计算 S2,2,所以应该有

S3,3=A3,3+S2,3+S3,2S2,2=5+18+159=29.

同样的道理,在已经预处理出二位前缀和后,要查询左上角为 (i1,j1)、右下角为 (i2,j2) 的子矩阵的和,可以计算

Si2,j2Si1,j2Si2,j1+Si1,j1.

这可以在 O(1) 时间内完成。

在二维的情形,以上算法的时间复杂度可以简单认为是 O(mn),即与给定数组的大小成线性关系。但是,当维度 k 增大时,由于容斥原理涉及的项数以指数级的速度增长,时间复杂度会成为 O(2kN),这里 k 是数组维度,而 N 是给定数组大小。因此,该算法不再适用。

逐维前缀和

对于一般的情形,给定 k 维数组 A,大小为 N,同样要求得其前缀和 S。这里,

Si1,,ik=i1i1ikikAi1,,ik.

从上式可以看出,k 维前缀和就等于 k 次求和。所以,一个显然的算法是,每次只考虑一个维度,固定所有其它维度,然后求若干个一维前缀和,这样对所有 k 个维度分别求和之后,得到的就是 k 维前缀和。

树上前缀和

sumi 表示结点 i 到根节点的权值总和。
然后:

  • 若是点权,x,y 路径上的和为 sumx+sumysumlcasumfalca

  • 若是边权,x,y 路径上的和为 sumx+sumy2sumlca

    LCA 的求法参见 最近公共祖先

题目描述

子矩阵的和

给定一个 nm 列的整数矩阵,以及 q 个询问。每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。

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

输入格式

  • 第一行包含三个整数 n, m, q
  • 接下来的 n 行,每行包含 m 个整数,表示整数矩阵。
  • 接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式

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

数据范围

  • 1n,m1000
  • 1q200000
  • 1x1x2n
  • 1y1y2m
  • 1000 矩阵内元素的值 1000

输入样例

3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4

输出样例

17
27
21

参考代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
typedef long long ll;
int a[N][N];
int s[N][N];
int n,m,q;
int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
         cin>>a[i][j],s[i][j]=0;
    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]+a[i][j]-s[i-1][j-1];
        }
    }
    while(q--)
    {
        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;
    }
}

差分

解释

差分是一种和前缀和相对的策略,可以当做是求和的逆运算。

这种策略的定义是令 bi={aiai1i[2,n]a1i=1

性质

  • ai 的值是 bi 的前缀和,即 an=i=1nbi
  • 计算 ai 的前缀和 sum=i=1nai=i=1nj=1ibj=i=1n(ni+1)bi

它可以维护多次对序列的一个区间加上一个数,并在最后询问某一位的数或是多次询问某一位的数。注意修改操作一定要在查询操作之前。

譬如使 [l,r] 中的每个数加上一个 k,即

blbl+k,br+1br+1k

其中 bl+k=al+kal1br+1k=ar+1(ar+k)

最后做一遍前缀和就好了。

C++ 标准库中实现了差分函数 std::adjacent_difference,定义于头文件 <numeric> 中。

题目描述

差分

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

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c

请你输出进行完所有操作后的序列。

输入格式

  • 第一行包含两个整数 nm
  • 第二行包含 n 个整数,表示整数序列。
  • 接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。

输出格式

共一行,包含 n 个整数,表示最终序列。

数据范围

  • 1n,m100000
  • 1lrn
  • 1000c1000
  • 1000 整数序列中元素的值 1000

输入样例

6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1

输出样例

3 4 5 3 4 2

参考代码:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n,m;
    cin>>n>>m;
    vector<int> a(n+1,0);
    vector<int> b(n+1,0);
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) b[i]=a[i]-a[i-1];
    while(m--)
    {
        int l,r,c;
        cin>>l>>r>>c;
        b[r+1]-=c;
        b[l]+=c;
    }
    for(int i=1;i<=n;i++) b[i]+=b[i-1];
    for(int i=1;i<=n;i++) cout<<b[i]<<" \n"[i==n];
}

二维差分

差分矩阵

题目描述

输入一个 nm 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)(x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c

请你将进行完所有操作后的矩阵输出。

输入格式

  • 第一行包含整数 n,m,q
  • 接下来 n 行,每行包含 m 个整数,表示整数矩阵。
  • 接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。

输出格式

n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。

数据范围

  • 1n,m1000
  • 1q100000
  • 1x1x2n
  • 1y1y2m
  • 1000c1000
  • 1000 矩阵内元素的值 1000

输入样例

3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1

输出样例

2 3 4 1
4 3 4 1
2 2 2 2

参考代码:

#include<bits/stdc++.h>
using namespace std;
int a[1010][1010];
int b[1010][1010];
int n,m,q;
int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++)
        cin>>a[i][j];
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];
        }
    }
    while(q--)
    {
        int x1,y1,x2,y2,c;
        cin>>x1>>y1>>x2>>y2>>c;
        b[x1][y1]+=c;
        b[x2+1][y1]-=c;
        b[x1][y2+1]-=c;
        b[x2+1][y2+1]+=c;
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j];
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
          cout<<a[i][j]<<" \n"[j==m];
    }
    
}

习题

前缀和:


二维/多维前缀和:


树上前缀和:


差分:


树上差分:


posted @   Violet_fan  阅读(300)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示