前缀和
引入——数组
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
数组都是从 0 开始编号的,但你是否下意识地想过,为什么数组要从 0 开始编号,而不是从 1 开始呢?
计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:
a[i]_address = base_address + i * data_type_size
引入——数组求和
// 局部代码
int sum = 0;
int l, r; // 数组求和下标索引范围
int arr[N]; // 此处略去了对数组元素的赋值
for(int i = l; i <= r; ++i)
{
sum += arr[i];
}
上面是进行一次求和,如果要多次进行求和?
int t;
cin >> t;
int arr[N];
int n;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> arr[i];
}
while(t--)
{
int l, r;
cin >> l >> r;
int sum = 0;
for(int i = l; i <= r; ++i)
{
sum += arr[i];
}
}
引入——时间复杂度
简单的来讲,可以理解为代码运行的次数…
第二段的时间复杂度,最坏情况为O(tn + n)
。如果 t
很大,比如1e8
,而 n
也很大,也为 1e8
。那么,这段代码的运行次数在 1e16
的情况。
假如现在有一台机子,一秒最多运行 1e8
次,那么以当前这种情况就需要运行 1e8
s。显然时间消耗太长了需要改进…
引入——前缀和
前缀和就是从第一个元素开始到当前下标 i
所有元素的和。我们还以代码来演示。
int t;
cin >> t;
int arr[N],sumn[N];
int n;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> arr[i];
sumn[i] = sumn[i - 1] + arr[i];
}
while(t--)
{
int l, r;
cin >> l >> r;
cout << sumn[r] - sumn[l - 1];
}
通过这一段修改,代码的复杂度就降到了O(n + t)
,这样如果还是刚才那个数据的话,只需要2
s。
例题
[蓝桥杯 2022 省 A] 求和
使用前缀和之前
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 3;
int arr[N], n;
int main()
{
// cin >> n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
{
// cin >> arr[i];
scanf("%d", &arr[i]);
}
int sum = 0;
for(int i = 1; i <=n; ++i)
{
for(int j = i + 1; j <= n; ++j)
{
sum += arr[i] * arr[j];
}
}
// cout << sum;
printf("%d\n", sum);
return 0;
}
测试结果
使用前缀和之后
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
int n;
scanf("%d", &n);
vector<int> nums(n);
for (register int i(0); i < n; ++i)
{
scanf("%d", &nums[i]);
}
vector<ll> temp(n);
temp[0] = nums[0];
for (register int i(1); i < n; ++i)
{
temp[i] = temp[i - 1] + nums[i];
}
ll res = 0;
for (register int i(0); i < n - 1; ++i)
{
res += nums[i] * (temp[n - 1] - temp[i]);
}
printf("%lld\n",res);
return 0;
}
测试结果
对比图
B3612 【深进1.例1】求区间和
使用前缀和之前
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 3;
int n, m, arr[N];
int main()
{
cin >> n;
// scanf("%d", &n);
for(int i = 1; i <= n; ++i)
{
cin >> arr[i];
// scanf("%d", &arr[i]);
}
cin >> m;
for(int i = 1; i <= m; ++i)
{
int l, r;
cin >> l >> r;
int res = 0;
for(int i = l; i <= r; ++i)
{
res += arr[i];
}
cout << res << "\n";
// printf("%d\n", res);
}
}
运行结果
使用前缀和之后
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 3;
int n, m, arr[N], sumn[N];
int main()
{
cin >> n;
// scanf("%d", &n);
for(int i = 1; i <= n; ++i)
{
cin >> arr[i];
// scanf("%d", &arr[i]);
sumn[i] = sumn[i - 1] + arr[i];
}
cin >> m;
for(int i = 1; i <= m; ++i)
{
int l, r;
cin >> l >> r;
cout << sumn[r] - sumn[l - 1] << "\n";
// printf("%d\n", sumn[r] - sumn[l - 1]);
}
}
运行结果
对比图
P2697 宝石串
P2697 宝石串
思路:将绿球视为 -1
,红球视为 1
,计算这个区间和。如果某一段区间的和为 0
,那么这一段的宝石就是稳定的宝石串,从中找出最长的即可。
题目代码(这段代码写麻烦了,建议跳过这个看下一个,这个太丑陋了)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 3;
typedef pair<int, int> PII;
string str;
int sumn[N];
map<int, PII> mp;
int main()
{
cin >> str;
int n = str.length();
for (int i = 1; i <= n; ++i)
{
if (str[i - 1] == 'G')
sumn[i] = sumn[i - 1] - 1;
else
sumn[i] = sumn[i - 1] + 1;
}
for (int i = 1; i <= n; ++i)
{
if (sumn[i] == 0)
mp[0] = {i, i};
else
{
if(mp.find(sumn[i]) != mp.end())
{
int f = mp[sumn[i]].first;
mp[sumn[i]] = {f, i};
}
else{
mp[sumn[i]] = {i, i};
}
}
}
int res = 0;
for(auto x : mp)
{
if(x.first == 0)
{
res = max(res, x.second.first);
}
else
{
res = max(res, x.second.second - x.second.first);
}
}
cout << res << "\n";
}
简洁点的代码,C语言选手也能很好看懂的
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 3;
string str;
int sumn[N], record[N];
int main()
{
cin >> str; // 对题目输入的读取
int n = str.length();
int minn = INT_MAX, res = 0, maxn = INT_MIN;
for (int i = 1; i <= n; ++i)
{
if (str[i - 1] == 'G')
sumn[i] = sumn[i - 1] - 1;
else
sumn[i] = sumn[i - 1] + 1;
minn = min(minn, sumn[i]);
}
for (int i = 1; i <= n; ++i)
if (sumn[i] == 0)
res = max(res, i);
else
if (record[sumn[i] + minn] == 0)
record[sumn[i] + minn] = i;
else
res = max(res, i - record[sumn[i] + minn]);
printf("%d", res);
}
进阶——二维前缀和
刚才是一维数组的处理情况,那么如果是二维数组该如何处理呢?
二维数组的前缀和是为了方便我们计算范围从 x1 行到 x2 行,y1列到 y2 列这个范围内矩阵元素的和,比如下方这个二维数组:
那么对于上面的二维数组,它的二维前缀和数组该怎么去求呢?
第 i
行第j
列的二维前缀和,在原本第 i
行第j
列值的基础上,还需要加上在它之前已经处理好的第 i-1
行第j
列的前缀和 与 第 i
行第j - 1
列的前缀和,并且因为这两个前缀和都同时覆盖了 第i-1
行 与 第 j-1
列的前缀和,所以需要减去一份这一块的前缀和。表达式如下:
// sumn数组是二维前缀和数组,arr数组是原数组
sumn[i][j] = arr[i][j] + sumn[i-1][j] + sumn[i][j - 1] - sumn[i - 1][j - 1];
有了前缀和数组之后,我们如何获得一个子矩阵范围内的和呢?大家是否还记得小时候学的计算一个矩阵内的矩阵面积。已知橘黄色区域面积、蓝色区域面积、黄色区域面积、粉色区域面积,如何去计算红色区域面积?很明显,橘黄色区域面积 - 蓝色区域面积 - 黄色区域面积 + 粉色区域面积(因为粉色区域被蓝色和黄色同时覆盖),二维前缀和同理。
二维前缀和部分代码
int sumn[N][N], t;
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j <= m; ++j)
{
cin >> t;
sumn[i][j] = sumn[i - 1][j] + sumn[i][j - 1] + t - sumn[i - 1][j - 1];
}
}
// 从r1,c1,r2,c2区域的所有原数组之和,其中r1 < r2,c1 < c2
sumn[r2][c2] - sumn[r2][c1 - 1] - sumn[r1 - 1][c2] + sumn[r1 - 1][c1 - 1];
细心的大佬应该发现了,在我上方代码中未出现之前提到的原数组——arr数组。在我们日常处理二位前缀和的时候,如果原数组在之后的处理中没有任何作用了,可以只声明一个二维前缀和数组。如果出题人限制了空间大小,而且数据也比较大的时候,可以通过这个办法减少多余的空间。
P1387 最大正方形
求出所给数组的前缀和,若区域和等于区域面积,则说明这是一个符合题意的结果
#include <bits/stdc++.h>
using namespace std;
const int N = 103;
int n, m, arr[N][N], sum[N][N], res = 1, i, j;
int main()
{
scanf("%d%d", &n, &m);
for (i = 1; i <= n; ++i)
{
for (j = 1; j <= m; ++j)
{
scanf("%d", &arr[i][j]);
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + arr[i][j];
}
}
int temp = 2;
while (temp <= min(n, m))
{
for (i = temp; i <= n; ++i)
{
for (j = temp; j <= m; ++j)
{
if(sum[i][j] - sum[i-temp][j] - sum[i][j - temp] + sum[i - temp][j - temp] == temp * temp)
{
res = max(res, temp);
}
}
}
++temp;
}
printf("%d\n", res);
return 0;
}