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

一、基本原理以及实现

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 

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 

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 

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 

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

你可能感兴趣的:(算法,#,基础算法,算法,前缀和,差分)