算法笔记:前缀和 & 差分

· · 算法·理论

前缀和

适用范围:区间求和

一维

有一个长度为 n 的数列 a,有 q 次询问,每次询问 lr,要求输出 \sum\limits_{i=l}^{r}a_i

首先考虑暴力,每次询问时:

int s=0;
for(int i=l;i<=r;i++)s+=a[i];
cout<<s<<'\n';

时间复杂度分析:最劣时,每次都 l=1,r=n,则时间复杂度为 O(nq)

那么我们怎么优化呢?

我们可以再考虑一个数组 s 存储 a 的前缀和,即

s_i=\sum\limits_{j=1}^ia_j

稍作变形便可得出递推式:

s_i=\begin{cases}a_i&i=1\\s_{i-1}+a_i&i>1\end{cases}

那么这样有什么用呢?

注意到,\sum\limits_{i=l}^ra_i=\sum\limits_{i=1}^ra_i-\sum\limits_{i=1}^{l-1}a_i=s_r-s_{l-1},这样便将 O(n) 的遍历变成了 O(1) 的计算,总复杂度直接降为 O(n+q)

::::info[例 参考代码]

#include<bits/stdc++.h>
#define int long long
#define N 10000005//本处以n<=1e7举例 
using namespace std;
int a[N],s[N],n,q;
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>q;
    for(int i=1;i<=n;i++){cin>>a[i];s[i]=s[i-1]+a[i];}
    while(q--){
        int l,r;cin>>l>>r;
        cout<<s[r]-s[l-1]<<'\n';
    }
    return 0;
}

::::

二维

有一个 nm 列的二维数组 a,有 q 次询问,每次询问 x_1,y_1,x_2,y_2,求 \sum\limits_{i=x_1}^{x_2}\sum\limits_{j=y_1}^{y_2}a_{i,j}

本题与上一例极为类似,只是改成了二维。

先考虑暴力,每次 O(nm) 的遍历数组,总时间复杂度 O(qmn)

类似地,设 s_{i,j}=\sum\limits_{k=1}^i\sum\limits_{l=1}^ja_{k,l},易得递推式为:

s_{i,j}=s_{i-1,j}+s_{i,j-1}-s_{i-1,j-1}+a_{i,j}

可以参考图来理解(绿色部分为黄蓝的重叠处):

预处理好 s 后,每次询问时,类似预处理的推导,易得:

\sum\limits_{i=x_1}^{x_2}\sum\limits_{j=y_1}^{y_2}a_{i,j}=s_{x_2,y_2}-s_{x_1-1,y_2}-s_{x_2,y_1-1}+s_{x_1-1,y_1-1}

复杂度优化至 O(nm+q)

差分

适用范围:区间整体加减

一维

有一个长度为 n 的数列 a,有 q 次操作,每次输入 lrw,要求对于每一个 i\in[l,r],都将 a_i 的值加上 w,最后输出 a

暴力做法为 O(nq),可以使用差分进行优化。

定义差分数组 d,其中

\begin{cases}a_i&i=1\\a_i-a_{i-1}&i>1\end{cases}

为什么要把差分和前缀和放在一起呢?

我们对差分数组求前缀和,设其为 p

p_i=\sum\limits_{j=1}^id_j=\sum\limits_{j=1}^i(a_j-a_{j-1})=a_i

于是我们得到一个重要的结论:差分数组的前缀和数组为原数组。

类似的,还可以得到:前缀和数组的差分数组为原数组。

所以:前缀和和差分互为逆运算。

回到例题,对于一次操作,设操作后的原数组和差分数组分别为 a^\primeb^\prime

分类讨论:

综上所述,对于 [l,r] 的区间整体加 w 的修改,仅需对差分数组 d 进行如下修改:

d[l]+=w;
d[r+1]-=w;
::::info[例 参考代码] ```cpp #include<bits/stdc++.h> #define int long long #define N 10000005//本处以n<=1e7举例 using namespace std; int a[N],d[N],n,q; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n>>q; for(int i=1;i<=n;i++){cin>>a[i];d[i]=a[i]-a[i-1];} while(q--){ int l,r,w;cin>>l>>r>>w; d[l]+=w; d[r+1]-=w; } for(int i=1;i<=n;i++){ a[i]=a[i-1]+d[i]; cout<<a[i]<<' '; } return 0; } ``` :::: 时间复杂度:$O(n+q)$。 ## 二维 > **例** 有一个 $n$ 行 $m$ 列的二维数组 $a$,有 $q$ 次操作,每次操作输入 $x_1,y_1,x_2,y_2,w$,对于任意的 $i\in[x_1,x_2]$ 和 $j\in[y_1,y_2]$,将 $a_{i,j}$ 加上 $w$,最后输出 $a$。 我们已经知道在一维中,差分和前缀和互为逆运算。类比过来,设差分数组 $d$: $$d_{i,j}=a_{i,j}-a_{i-1,j}-a_{i,j-1}+a_{i-1,j-1}$$ 对照一维前缀和及差分的迁移,不难想出二维的迁移,这里不写推导过程,作为思考题留给读者,直接给出操作代码: ```cpp d[x_1][y_1]+=w; d[x_2+1][y_1]-=w; d[x_1][y_2+1]-=w; d[x_2+1][y_2+1]+=w; ``` 最后对差分数组求前缀和即可。 时间复杂度:$O(mn+q)$。 # 课后作业 [算法笔记:前缀和 & 差分 课后作业](https://www.luogu.com.cn/training/854348)。