/** 鼠标样式 **/

Shu-How Zの小窝

Loading...

前缀和与差分

第一讲 前缀和与差分

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

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

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

先来看一道例题

前缀和

题目描述

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

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

输入格式

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

输出格式

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

数据范围

  • \(1 \leq l \leq r \leq n\)
  • \(1 \leq n, m \leq 100000\)
  • \(-1000 \leq\) 数列中元素的值 \(\leq 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\)项的和为\(S_x\)

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

  • \(S_1\) = \(a_1\)

  • \(S_2\) = \(a_1\) + \(a_2\)

  • \(S_3\) = \(a_1\) + \(a_2\) + \(a_3\)

  • \(...\)

  • \(S_{n-1}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{n-1}\)

  • \(S_{n}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{n-1}\) + \(a_{n}\)

知道前缀和,便可以知道任意区间内所有数之和,即\(Sum_{l,r}\) = \(S_r\) - \(S_{l-1}\):

  • \(S_{l-1}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{l-1}\)
  • \(S_{r}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{r-1}\) + \(a_{r}\)
  • \(Sum_{l,r}\) = \(a_l\) + \(a_{l+1}\) + \(a_{l+2}\) + \(...\) + \(a_{r}\) = \(S_r\) - \(S_{l-1}\);

例:

  • \(Sum_{3,9}\) = \(S_9\) - \(S_2\)
  • \(Sum_{3,7}\) = \(S_7\) - \(S_2\)
  • \(Sum_{3,3}\) = \(S_3\) - \(S_2\)

如何求前缀和?

首先初始化\(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\times n\) 的二维数组 \(A\),要求出其前缀和 \(S\)。那么,\(S\) 同样是大小为 \(m\times n\) 的二维数组,且

\[S_{i,j} = \sum_{i'\le i}\sum_{j'\le j}A_{i',j'}. \]

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

\[S_{i,j} = A_{i,j} + S_{i-1,j} + S_{i,j-1} - S_{i-1,j-1}. \]

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

考虑一个具体的例子。

二位前缀和示例

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

\[S_{3,3} = A_{3,3} + S_{2,3} + S_{3,2} - S_{2,2} = 5 + 18 + 15 - 9 = 29. \]

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

\[S_{i_2,j_2} - S_{i_1,j_2} - S_{i_2,j_1} + S_{i_1,j_1}. \]

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

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

逐维前缀和

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

\[S_{i_1,\cdots,i_k} = \sum_{i'_1\le i_1}\cdots\sum_{i'_k\le i_k} A_{i'_1,\cdots,i'_k}. \]

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

树上前缀和

\(\textit{sum}_i\) 表示结点 \(i\) 到根节点的权值总和。
然后:

  • 若是点权,\(x,y\) 路径上的和为 \(\textit{sum}_x + \textit{sum}_y - \textit{sum}_\textit{lca} - \textit{sum}_{\textit{fa}_\textit{lca}}\)

  • 若是边权,\(x,y\) 路径上的和为 \(\textit{sum}_x + \textit{sum}_y - 2\cdot\textit{sum}_{lca}\)

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

题目描述

子矩阵的和

给定一个 \(n\)\(m\) 列的整数矩阵,以及 \(q\) 个询问。每个询问包含四个整数 \(x_1, y_1, x_2, y_2\),表示一个子矩阵的左上角坐标和右下角坐标。

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

输入格式

  • 第一行包含三个整数 \(n\), \(m\), \(q\)
  • 接下来的 \(n\) 行,每行包含 \(m\) 个整数,表示整数矩阵。
  • 接下来 \(q\) 行,每行包含四个整数 \(x_1, y_1, x_2, y_2\),表示一组询问。

输出格式

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

数据范围

  • \(1 \leq n, m \leq 1000\)
  • \(1 \leq q \leq 200000\)
  • \(1 \leq x_1 \leq x_2 \leq n\)
  • \(1 \leq y_1 \leq y_2 \leq m\)
  • \(-1000 \leq\) 矩阵内元素的值 \(\leq 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;
    }
}

差分

解释

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

这种策略的定义是令 \(b_i=\begin{cases}a_i-a_{i-1}\,&i \in[2,n] \\ a_1\,&i=1\end{cases}\)

性质

  • \(a_i\) 的值是 \(b_i\) 的前缀和,即 \(a_n=\sum\limits_{i=1}^nb_i\)
  • 计算 \(a_i\) 的前缀和 \(sum=\sum\limits_{i=1}^na_i=\sum\limits_{i=1}^n\sum\limits_{j=1}^{i}b_j=\sum\limits_{i=1}^n(n-i+1)b_i\)

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

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

\[b_l \leftarrow b_l + k,b_{r + 1} \leftarrow b_{r + 1} - k \]

其中 \(b_l+k=a_l+k-a_{l-1}\)\(b_{r+1}-k=a_{r+1}-(a_r+k)\)

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

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

题目描述

差分

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

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

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

输入格式

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

输出格式

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

数据范围

  • \(1 \leq n, m \leq 100000\)
  • \(1 \leq l \leq r \leq n\)
  • \(-1000 \leq c \leq 1000\)
  • \(-1000 \leq\) 整数序列中元素的值 \(\leq 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];
}

二维差分

差分矩阵

题目描述

输入一个 \(n\)\(m\) 列的整数矩阵,再输入 \(q\) 个操作,每个操作包含五个整数 \(x_1, y_1, x_2, y_2, c\),其中 \((x_1, y_1)\)\((x_2, y_2)\) 表示一个子矩阵的左上角坐标和右下角坐标。

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

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

输入格式

  • 第一行包含整数 \(n, m, q\)
  • 接下来 \(n\) 行,每行包含 \(m\) 个整数,表示整数矩阵。
  • 接下来 \(q\) 行,每行包含 5 个整数 \(x_1, y_1, x_2, y_2, c\),表示一个操作。

输出格式

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

数据范围

  • \(1 \leq n, m \leq 1000\)
  • \(1 \leq q \leq 100000\)
  • \(1 \leq x_1 \leq x_2 \leq n\)
  • \(1 \leq y_1 \leq y_2 \leq m\)
  • \(-1000 \leq c \leq 1000\)
  • \(-1000 \leq\) 矩阵内元素的值 \(\leq 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 @ 2024-11-17 00:30  Violet_fan  阅读(194)  评论(0编辑  收藏  举报