数据结构与算法入门指南 - 前缀和与差分

567 阅读2分钟

数据结构与算法入门指南 - 目录

前缀和

如果我们要求数组中某一段区间(L, R)的和,用普通的循环每次查询都需要从L到R循环一遍,那有没有一种更快速的方法呢?当然有,那就是前缀和啦。

一维前缀和

前缀和,顾名思义就是前缀的和,前缀和数组中的每一位都是原数组中从头到目前的和,也就是S[i] = A[1] + A[1] + ... + A[i],其中S是前缀和数组,A是原数组。

写成代码就是(存数的时候从下标为1开始存比较好):

for (int i = 1; i <= len; i++)
		S[i] = S[i - 1] + A[i]; // 目前前缀和 = 前i-1位的前缀和+目前原数组的值
cout << S[R] - S[L - 1]; // 表示A[1] + ... + A[L]

那求出前缀和数组对于 求原数组中某一段区间的和 有什么帮助呢?

通过观察我们可以发现一个公式:S[R] - S[L - 1] = A[L] + ... + A[R]

具体的推导过程:

S[R]=A[1]+...+A[R]S[R] = A[1] + ... + A[R]
S[L1]=A[1]+...+A[L1]S[L - 1] = A[1] + ... + A[L - 1]
S[R]S[L1]=(A[1]+...+A[R])(A[1]+...+A[L1])=A[L]+...A[R]S[R] - S[L - 1] \\ = (A[1] + ... + A[R]) - (A[1] + ... + A[L - 1]) \\ = A[L] + ... A[R]

用图表示就是:

红线表示A[1] + ... + A[R]。简单来说就是S[R]减掉前面L - 1个元素就得到了A[L] + ... + A[R]

利用前缀和数组可以用公式直接计算出某个区间的和,比原来要执行R - L + 1次快多了!


我们来看一道题试试:

P1115 最大子段和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这道题如果用最简单的做法来做,也就是遍历所有可能的区间,复杂度为O(n^3),会超时。

for (int i = 1; i <= n; i++)
		for (int j = i + 1; j <= n; j++)
		{
			int sum = 0;
			for (int k = i; k <= j; k++) sum += A[k];
			ans = max(ans, sum);
		}

那么运用到我们求前缀和的做法可以很容易求得某个区间的和,但要遍历完所有可能的区间,也需要O(n^2)的复杂度。

for (int i = 1; i <= n; i++) //求前缀和
		S[i] = S[i - 1] + A[i];

for (int i = 1; i <= n; i++)
    for (int j = i + 1; j <= n; j++)
    	ans = max(ans, S[j] - S[i - 1]); //S[j] - S[i - 1] = A[i] + ... + A[j]

我们仔细想想,目前已经知道了前缀和,那么要求最大字段和,只需要减去这个前缀和区间内最小的前缀即可,然后一一比对出其中最大的即可,需要额外维护一个最小前缀和数组Min。

完整代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10, INF = 0x3f3f3f3f;
int n, A[N], S[N], Min[N];

int main()
{
	memset(Min, INF, sizeof Min); //Min数组默认初始化为一个很大的数
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> A[i];
	for (int i = 1; i <= n; i++)
	{
		S[i] = S[i - 1] + A[i]; //计算前缀和
		Min[i] = min(Min[i - 1], S[i]); //计算该前缀和中 最小的前缀和是多少
	}

	int ans = A[1];
	for (int i = 1; i <= n; i++)
		ans = max(ans, S[i] - Min[i - 1]); //对比每一段前缀中可能的最大值

	cout << ans;

	return 0;
}

二维前缀和——子矩阵的和

在二维数组中我们也能用到前缀和,每个元素表示(1, 1)(i, j)方格内的和。

当有了这样一个二维前缀和的数组后,我们能干什么呢?答案就是用公式可以直接算出任意矩形内的和!

那么我们该怎么求这个二维前缀和数组呢?我们就需要依次遍历元素,把目前元素上边一格的前缀和与左边一格的前缀和加上,但加完之后还会有一个重叠部分,需要减去;再加上目前元素就是前缀和了。可能文字有点抽象,可以参考一下下面的配图。

具体怎么写呢?代码:

for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            		 //目前  + 上面一格     + 左边一格     - 重复部分
            S[i][j] = A[i][j] + S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1];

那么有这么一个二维前缀和数组,怎么快速求任意方格的和呢?

(配合下图食用)用B覆盖的蓝色区块做减法,把A上部分和A左部分删去即可,但上部分和左部分会有重叠,也就是会删多一次,加回来即可。

设A的坐标为(x1, y1),B的坐标为(x2, y2),那么要求的子矩阵的和为:

S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1]

来做道题消化一下吧~

P1719 最大加权矩形 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目的大意就是求 子矩阵最大的和为多少,那么我们开始吧。

如果我们用枚举来遍历所有可能的子矩阵,那么复杂度将会是O(n^6)!肯定会超时,所以我们需要用二维前缀和来优化它。

for (int a = 1; a <= n; a++)
    for (int b = 1; b <= m; b++)
        for (int c = 1; c <= n; c++)
            for (int d = 1; d <= m; d++)
            {
                int sum = 0;
                for (int i = a; i <= c; i++)
                	for (int j = b; j <= d; j++)
                        sum += A[i][j];
                ans = max(ans, sum);
    		}

先预处理出前缀和数组,再遍历每个可能的矩阵即可。时间复杂度为O(n^4)

完整代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 150;
int S[N][N];

int main()
{
    int n, x, ans = INT_MIN; cin >> n;
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= n; j ++)
        {
            cin >> x; //读入目前格子
            S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + x; //求前缀和数组
        }
    
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= n; j ++)
            for (int x = 1; x <= i; x ++)
                for (int y = 1; y <= j; y ++)
                    //遍历所有可能的矩阵(也就是遍历A点坐标和B点坐标可能的组合)
                    ans = max(ans, S[i][j] - S[x - 1][j] - S[i][y - 1] + S[x - 1][y - 1] );
    
    cout << ans;
    
    return 0;
}

差分

如果需要快速让数组中某个区间加上一个数,就需要用到差分。

一维差分

首先我们需要一个差分数组,这个差分数组的前缀和数组刚好就是原数组,有点类似前缀和的逆操作?

怎么求这个差分数组B呢?原数组目前元素减去上一个元素的值 就是 目前差分数组的值了。

有点绕,用代码来看看:

for (int i = 1; i <= n; i++)
    B[i] = A[i] - A[i - 1];

有了这个差分数组后?怎么才能快速的让某个区间加上一个数呢?

我们知道最后要求前缀和数组来还原出原数组,那么前缀和数组又有什么性质呢?

只要给某个数加上x,那么后面所有的数在求前缀和数组的时候都会被加上x。

根据这个性质,在(L, R)区间内加上一个数可以用 B[L] += x, B[R + 1] -= x来实现,让数组从L开始到最后都加上x,但由于是加到最后,可能超出了R的范围,所以我们需要让R后面的都减去x,保持原来的值。

这样,我们就能用O(1)的时间来对某个区间增减,再用O(n)的时间来还原原数组。

看看代码的实现过程:

int A[11] = { 0,1,2,3,4,5,6,7,8,9,10 }, B[11] = { 0 };
for (int i = 1; i <= 10; i++) B[i] = A[i] - A[i - 1];

int L = 2, R = 5, x = 3; //需要在(2, 5)的区间上都加上3
B[L] += x, B[R + 1] -= x; 

for (int i = 1; i <= 10; i++)
    cout << (B[i] += B[i - 1]) << ',';
//1,5,6,7,8,6,7,8,9,10

二维差分——差分矩阵

既然差分数组是可以快速在某个区间上增减值,那么差分矩阵就是快速在某个子矩阵内增减值了。

如果我们要在差分矩阵内实现对某个子矩阵增减值,该怎么做呢?

每次在差分矩阵中增加值x的时候,因为是用前缀和来还原,所以会让(x, y)到(n, m)区间内的值都加上x,这时候我们就需要把超出范围的值给减回来。

设A(x1, y1)为需要加值的子矩阵的左上角,B(x2, y2)为需要加值的子矩阵的右下角。

给区域加值的代码为:

B[x1][y1] += x; //给A到最后的矩阵加x
B[x1][y2 + 1] -= x; //减去超出范围的右边
B[x2 + 1][y1] -= x; //减去超出范围的下边
B[x2 + 1][y2 + 1] += x; //把重复删减的部分给加回来

那么我们要怎么用前缀和求回原数组呢?好像讲过了,怕忘记再讲一遍~

(配合下图食用)目前的A需要加上 上部分的前缀和 与 左部分的前缀和,然而又又又有重叠部分需要我们删去,把重叠部分删除即可。

设A的坐标为(i, j),则用代码表示其前缀和为:

B[i][j] = B[i][j] + B[i - 1][j] + B[i][j - 1] - B[i - 1][j - 1];
//        本身    + 上部分       + 左部分       - 重复部分

这个前缀和数组就能还原回原数组修改完值后的样子了。

看个实例代码:

#include <bits/stdc++.h>
using namespace std;

//原数组,下标从1开始
int A[6][6] =
{
	{0, 0, 0, 0, 0, 0},
	{0, 1, 2, 3, 4, 5},
	{0, 6, 7, 8, 9,10},
	{0,11,12,13,14,15},
	{0,16,17,18,19,20},
	{0,21,22,23,24,25}
};

int B[7][7]; //前缀和数组(需要开多一位,因为x + 1或 y + 1这类操作可能会越界)

void insert(int x1, int y1, int x2, int y2, int x)
{
	B[x1][y1] += x; //给(x1, y1)到(n, m)的子矩阵都加上x (n, m为数组大小)
	B[x1][y2 + 1] -= x; //减去超出范围的右边
	B[x2 + 1][y1] -= x; //减去超出范围的下边
	B[x2 + 1][y2 + 1] += x; //重叠部分会多减一次,加回来。
}

int main()
{
	for (int i = 1; i <= 5; i++)
		for (int j = 1; j <= 5; j++)
			insert(i, j, i, j, A[i][j]); //在(i, j)到(i, j)的子矩阵,也就是(i, j)本身加上A[i][j]

	insert(2, 2, 4, 3, 5); //在(2, 2)到(4, 3)的子矩阵中加上5

	for (int i = 1; i <= 5; i++)
		for (int j = 1; j <= 5; j++)
		{
			//求前缀和数组
			B[i][j] = B[i][j] + B[i - 1][j] + B[i][j - 1] - B[i - 1][j - 1];
		}

	// B[][] = 
	// 1  2  3  4  5
	// 6 12 13  9 10
	//11 17 18 14 15
	//16 22 23 19 20
	//21 22 23 24 25

	return 0;
}

题目

待增加

【算法2-1】前缀和与差分 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)


如果你觉得写得还不错,可以点个赞,关个注支持一下😊。