算法中的『前缀和』及『差分』思想详解

一、基本原理以及实现

1. 一维前缀和

  • 一维前缀和的定义:对于一个从下标从『1』开始的长度为『 n n n』的一维数组 『 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an』,前缀和的计算公式为『 S i = a 1 + a 2 + . . . + a i S_i=a_1+a_2+...+a_i Si=a1+a2+...+ai』。
    • 如何求解『 S i S_i Si
      S[0] = 0;
      for (int i = 1; i <= n; ++i) {
         S[i] = S[i - 1] + a[i];
      }
      
    • 作用:快速的求解原数组中任意一段 [l,r] 的和『 S = S r − S l − 1 S=S_r-S_{l-1} S=SrSl1』,计算的时间复杂度为 O ( 1 ) O(1) O(1)
    • 为什么原数组下标必须从1开始:定义『 S 0 = 0 S_0=0 S0=0』可以更好地处理边界问题,统一公式为『 S = S r − S l − 1 S=S_r-S_{l-1} S=SrSl1』。例如计算『 S 10 S_{10} S10』也可以写成『 S 10 = S 10 − S 0 S_{10}=S_{10}-S_0 S10=S10S0』。

2. 二维前缀和

  • 二维前缀和的定义:对于一个从下标从『1』开始的大小为『 n × m n \times m n×m』的二维数组 『 a i j a_{ij} aij』,前缀和的计算公式为『 S i j = a 11 + a 12 + . . . + a 1 j + a 21 + a 22 + . . . + a 2 j + . . . + a i 1 + a i 2 + . . . + a i j S_{ij}=a_{11}+a_{12}+...+a_{1j}+a_{21}+a_{22}+...+a_{2j}+...+a_{i1}+a_{i2}+...+a_{ij} Sij=a11+a12+...+a1j+a21+a22+...+a2j+...+ai1+ai2+...+aij』,即为其左上角所有元素的和。
    • 如何求解『 S i j S_{ij} Sij
      S[0][0] = 0; S[0][1] = 0; S[1][0] = 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] - S[i - 1][j - 1] + a[i][j];
      }
      
    • 作用:快速的求解原二维数组中任意一块矩形 [ l 1 [l_1 [l1 ~ l 2 , r 1 l_2,r_1 l2,r1 ~ r 2 ] r_2] r2] l 2 > l 1 , r 2 > r 1 l_2>l_1,r_2>r_1 l2>l1,r2>r1)的和『 S = S l 2 r 2 − S ( l 1 − 1 ) r 2 − S l 2 ( r 1 − 1 ) + S ( l 1 − 1 ) ( r 1 − 1 ) S=S_{l_2r_2}-S_{(l_1-1)r_2}-S_{l_2(r_1-1)}+S_{(l_1-1)(r_1-1)} S=Sl2r2S(l11)r2Sl2(r11)+S(l11)(r11)』,计算的时间复杂度为 O ( 1 ) O(1) O(1)

3. 一维差分

  • 一维差分的定义:对于一个从下标从『1』开始的长度为『 n n n』的一维数组 『 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an』,构造一个新数组『 b 1 , b 2 , b 3 , . . . , b n b_1,b_2,b_3,...,b_n b1,b2,b3,...,bn』,使得『 a i a_i ai』是『 b b b』数组的前缀和,即『 a i = b 1 + b 2 + . . . + b i a_i=b_1+b_2+...+b_i ai=b1+b2+...+bi』,此时『 b b b』数组称为『 a a a』数组的差分。
    • 如何构造『 b i b_i bi』数组:『 b 1 = a 1 , b 2 = a 2 − a 1 , b 3 = a 3 − a 2 , . . . , b n = a n − a n − 1 b_1=a_1,b_2=a_2-a_1,b_3=a_3-a_2,...,b_n=a_n-a_{n-1} b1=a1,b2=a2a1,b3=a3a2,...,bn=anan1
    • 作用:快速的求解原数组经过一系列操作后得到的新数组,操作具体为在原数组中任意一段 [l,r] 区间内的所有数加上一个常数『 c c c』。这一系列操作利用差分的思想可以直接对『 b b b』数组进行时间复杂度为 O ( 1 ) O(1) O(1)的计算即可,具体计算方法是『 b l + c ; b r + 1 − c b_l+c; b_{r+1}-c bl+c;br+1c』。此时『 b i b_{i} bi』数组的前缀和即为操作后的结果。
      void insert(int l, int r, int c) {
         b[l] += c;
         b[r + 1] -= c;
      }
      
    • b l + c ; b r + 1 − c b_l+c; b_{r+1}-c bl+c;br+1c』理解
      • b l + c b_l+c bl+c』使得『 a 1 ∗ = a 1 , a 2 ∗ = a 2 , . . . , a l ∗ = a l + c , a l + 1 ∗ = a l + 1 + c , . . . , a r ∗ = a r + c , a r + 1 ∗ = a r + 1 + c , . . . , a n ∗ = a n + c a_1^*=a_1,a_2^*=a_2,...,a_l^*=a_l+c,a_{l+1}^*=a_{l+1}+c,...,a_r^*=a_r+c,a_{r+1}^*=a_{r+1}+c,...,a_n^*=a_n+c a1=a1,a2=a2,...,al=al+c,al+1=al+1+c,...,ar=ar+c,ar+1=ar+1+c,...,an=an+c』;
      • 然后,『 b r + 1 − c b_{r+1}-c br+1c』使得『 a 1 ∗ = a 1 , a 2 ∗ = a 2 , . . . , a l ∗ = a l + c , a l + 1 ∗ = a l + 1 + c , . . . , a r ∗ = a r + c , a r + 1 ∗ = a r + 1 , a r + 2 ∗ = a r + 2 , . . . , a n ∗ = a n a_1^*=a_1,a_2^*=a_2,...,a_l^*=a_l+c,a_{l+1}^*=a_{l+1}+c,...,a_r^*=a_r+c,a_{r+1}^*=a_{r+1},a_{r+2}^*=a_{r+2},...,a_n^*=a_n a1=a1,a2=a2,...,al=al+c,al+1=al+1+c,...,ar=ar+c,ar+1=ar+1,ar+2=ar+2,...,an=an』,即完成了操作要求。
    • 注意:给定了『 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an』数组,我们其实可以看做是一个全『 0 0 0』数组进行一系列操作后得到的『 a a a』数组,此处的一系列操作为『 [ 1 , 1 ] + a 1 , [ 2 , 2 ] + a 2 , . . . , [ n , n ] + a n [1,1]+a_1,[2,2]+a_2,...,[n,n]+a_n [1,1]+a1,[2,2]+a2,...,[n,n]+an』。因此可以假定初始数组为全『 0 0 0』数组,显然『 b b b』数组也为全『 0 0 0』数组,因此『 b b b』数组的构造不是很重要。

4. 二维差分

  • 二维差分的定义:对于一个从下标从『1』开始的长度为『 n × m n \times m n×m』的二维数组 『 a i j a_{ij} aij』,构造一个新数组『 b i j b{ij} bij』,使得『 a i j a_{ij} aij』是『 b i j b_{ij} bij』的前缀和,此时『 b b b』数组称为『 a a a』数组的差分。
    • 如何构造『 b i j b_{ij} bij』数组:参考一维差分,我们可以假定初始二维数组为全『 0 0 0』数组,显然『 b b b』数组也为全『 0 0 0』二维数组。
    • 作用:快速的求解原二维数组经过一系列操作后得到的新二维数组,操作具体为在原二维数组中任意一个子矩阵内的所有数加上一个常数『 c c c』。这一系列操作利用差分的思想可以直接对『 b i j b_{ij} bij』数组进行时间复杂度为 O ( 1 ) O(1) O(1)的计算即可,具体计算方法是『 b l 1 r 1 + c ; b ( l 2 + 1 ) r 1 − c ; b l 1 ( r 2 + 1 ) − c ; b ( l 2 + 1 ) ( r 2 + 1 ) + c ; b_{l_1r_1}+c; b_{(l_2+1)r_1}-c;b_{l_1(r_2+1)}-c;b_{(l_2+1)(r_2+1)}+c; bl1r1+c;b(l2+1)r1c;bl1(r2+1)c;b(l2+1)(r2+1)+c;』。此时『 b i j b_{ij} bij』数组的前缀和即为操作后的结果。
    void insert(int x1, int y1, int x2, int y2, int c) {
        b[x1][y1] += c;
        b[x2 + 1][y1] -= c;
        b[x1][y2 + 1] -= c;
        b[x2 + 1][y2 + 1] += c;
    }
    

二、应用:模板题

(一维前缀和)AcWing 795. 前缀和

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int a[N], s[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    for (int i = 1; i <= n; 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. 子矩阵的和

#include <iostream>

using namespace std;

const int N = 1010;

int n, m, q;
int s[N][N], a[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]);

    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]; // 求前缀和

    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;
}

(一维差分)AcWing 797. 差分

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N];

void insert(int l, int r, int c) {
    b[l] += c;
    b[r + 1] -= c;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i ++ ) insert(i, i, a[i]);  // 这里由于假定初始数组全 0,因此需要先进行预操作

	 // 这里是题目要求的 m 次操作
    while (m -- )  {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }

    for (int i = 1; i <= n; i ++ ) b[i] += b[i - 1];  // 求解操作完后 b 数组的前缀和即为答案

    for (int i = 1; i <= n; i ++ ) printf("%d ", b[i]);

    return 0;
}

(二维差分)AcWing 798. 差分矩阵

#include <iostream>

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

void insert(int x1, int y1, int x2, int y2, int c) {
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

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]);

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            insert(i, j, i, j, a[i][j]);  // 这里由于假定初始数组全 0,因此需要先进行预操作

    while (q -- ) {
        int x1, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1, y1, x2, y2, c);
    }

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];  // 求解操作完后 b 数组的前缀和即为答案

    for (int i = 1; i <= n; i ++ ) {
        for (int j = 1; j <= m; j ++ ) printf("%d ", b[i][j]);
        puts("");
    }

    return 0;
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PanyCG_pc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值