数据结构与算法入门指南 - 目录
前缀和
如果我们要求数组中某一段区间(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]
具体的推导过程:
用图表示就是:
红线表示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)
如果你觉得写得还不错,可以点个赞,关个注支持一下😊。