前缀和和差分

394 阅读5分钟

前缀和

一维前缀和

定义:S[i] = a[1] + a[2] + ... a[i] a[l] + ... + a[r] = S[r] - S[l - 1]

int a[N], S[N];

for (int i = 1; i <= n; i++) S[i] = S[i - 1] + a[i];      // 给定数组a,初始化前缀和数组S
for (int i = 1; i <= n; i++)  {
    scanf("%d", &a[i])        // 非必须
    S[i] = S[i - 1] + a[i];   // 未给定数组a,可合并读入和初始化的过程
}

cout << S[r] - S[l - 1] << endl; //区间和的计算,查询                      
// 计算a[l] + ... + a[r]
//在一些不涉及a[i]的题目中,不必要存储a[i]的值,只需要存储S[i]就足够

一维前缀和的目的就是降低复杂度,计算区间和的复杂度由原本O(n)降低成为了O(1),相当于直接茶查找,不再需要遍历了。
数组a和S的第1个元素都不存储(下标为0),而从第2个元素开始存储(下标为1),因为下标为0的S0=a0我们默认为0,以方便计算。[当计算1至某个数时,计算Sn- S0即可]。

二维前缀和

由以为前缀和,想要快速求出子矩阵的和的时候,就要用上二维前缀和了。

S[i, j] = 第i行j列格子左上部分所有元素的和 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

image.png S[i,j]即为图1红框中所有数的的和为: S[i,j]=S[i,j−1]+S[i−1,j]−S[i−1,j−1]+a[i,j]S[i,j]=S[i,j−1]+S[i−1,j]−S[i−1,j−1]+a[i,j] (x1,y1),(x2,y2)这一子矩阵中的所有数之和为:S[x2,y2]−S[x1−1,y2]−S[x2,y1−1]+S[x1−1,y1−1]

//模板
int a[N][N], S[N][N];

// 给定数组a
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];//可以拆开写

// 没有给定数组a,需要读入并初始化前缀和数组,则可以合并读入和初始化的过程
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++) {
        scanf("%d", &a[i][j]);
        S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j];
    }

cout << S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1] << endl;      // 使用

同样假设数组a中行下标或列下标为0的项都是0,以方便计算。最重要的是复杂度由O(m * n)降为O(1)。[读入数组a和初始化前缀和数组S的过程可以合并在一起].

差分

一维差分

差分就是前缀和的逆运算。

首先给定一个原数组aa[1], a[2], a[3]....a[n];
然后我们构造一个数组b : b[1] ,b[2] , b[3].... b[i];
使得 a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i] 也就是说,a数组是b数组的前缀和数组,我们就把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。 我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

如何构造差分数组呢,使用最为直接的方法b[i] = a[i] - a[i - 1]

一维差分的作用,也是一个结论:给a数组中的[l,r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c,再求前缀和就行了。时间复杂度为O(1), 大大提高了效率。
代码:

int a[N], B[N];

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

// 初始化差分数组
for (int i = 1; i <= n; i++) {
    scanf("%d", &a[i]);
    insert(i, i, a[i]);
}

// 输出前缀和数组
for (int i = 1; i <= n; i++) {
    B[i] += B[i - 1];
    printf("%d ", B[i]);
}

二维差分

同样的分体,如果扩展到二维,我们需要让二维数组被选中的子矩阵中的每个元素的值加上c,是否也可以达到O(1)的时间复杂度?当然是可以的,用二维差分,与一维差分很相似,类比二维前缀和、原a数组中a[i][j]是差分数组b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。

画一个图来理解这个过程: image.png
b[x1][ y1 ] +=c , 让整个a数组中蓝色矩形面积的元素都加上了c。 b[x1,][y2+1]-=c , 让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。 b[x2+1][y1]- =c ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。 b[x2+1][y2+1]+=c;,对应图4,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,使其恢复。
这样的操作后再求二维前缀和,就使 矩阵都加c

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

// 构造(无需额外的数组a)
int tmp;
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
        scanf("%d", &tmp);
        insert(i, j, i, j, tmp);
    }
}

// 转换成二维前缀和数组
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];