算法介绍:
前缀和算法是一种用空间换事件的算法,常用于解决某些题目或作为高级算法的组成部分。它可以用于快速计算数组元素之和,通过预先计算数组中每个位置前所有元素的累加和,将这些部分和存储在一个新数组中,从而在需要计算某个区间的和时,可以通过简单的减法操作得到结果,而不必重新遍历整个区间。
第一题:求区间和
P8218 【深进1.例1】求区间和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P8218
解法一:暴力求解
第一步:根据要求输入所有值:
int n; cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
int m; cin >> m;
第二步:进行m次循环输出m个结果:每次循环需要进行lf到rt的遍历操作,进行区间累加和。
for (int i = 0; i < m; i++) {
int lf, rt; cin >> lf >> rt;
long long sum = 0;
for (int j = lf - 1; j < rt; j++) {
sum += vec[i];
}
cout << sum << endl;
}
最终代码:
#include <bits/stdc++.h>
using namespace std;
#define Long long long
int main() {
int n; cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
int m; cin >> m;
for (int i = 0; i < m; i++) {
int lf, rt; cin >> lf >> rt;
Long sum = 0;
for (int j = lf - 1; j < rt; j++) {
sum += vec[i];
}
cout << sum << endl;
}
return 0;
}
时间复杂度: 外层循环m次,内层循环每次rt-lf次,考虑最坏情况,内层循环rt-lf取n,时间复杂度为:O(m*n);m和n在同一数量级,可能取到O(n²)的情况。
解法二:前缀和求解
假设我们知道前n项和的公式,代入n我们就会知道前n项和的值。由数学数列的知识我们可以知到:如果我们想要求第m项到第n项的和,我们可以求出前n项和,求出前m-1项和,用前n项和减去前m-1项和的值,结果就是第m项到第n项的和。
换言之,假设我们数组第 i 项中放的就是前 i 项和的值,那么求区间和就只需要进行一次减法就能将问题解决。可我们又怎么将前 i 项和放到第 i 项中呢。由S[n]=a[n]+S[n-1]得知,第 i 项的值加上前i-1项的和,就是前i项的和。规定第0项为0,我们从第1项开始,s[1]=a[1]+s[0]......
代码怎么实现呢?
#include <bits/stdc++.h>
using namespace std;
#define Long long long
int main() {
int n; cin >> n;
vector<int> a(n+1);
vector<Long> S(n + 1);
a[0] = 0;S[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
S[i] = a[i] + S[i - 1];
}
int m; cin >> m;
for (int i = 0; i < m; i++) {
int lf, rt; cin >> lf >> rt;
cout << S[rt] - S[lf - 1] << endl;
}
return 0;
}
时间复杂度:很显然时间复杂度为O(n+m),但由于额外开了一个存放前 i 项和的数组S,此时空间复杂度成为了O(n)。
优化:优化前缀和
#include <bits/stdc++.h>
using namespace std;
#define Long long long
int main() {
int n; cin >> n;
vector<Long> S(n + 1);
S[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> S[i];
S[i] = S[i] + S[i - 1];
}
int m; cin >> m;
for (int i = 0; i < m; i++) {
int lf, rt; cin >> lf >> rt;
cout << S[rt] - S[lf - 1] << endl;
}
return 0;
}
有了前 i 项和的数组,我们就不需要原来的数组了,在该题中没有任何存在的价值了。那么我们将一开始的数组就当作前 i 项和的数组,我们输入第 i 个,就求出输入的值加上前 i-1 项的和,在放到 第 i 项中。就会改变空间复杂度为O(1)。
第二题,最大子段和
P1115 最大子段和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1115
解法一:暴力求解
首先输入题目要求的所有信息。
对每一个子段都进行遍历。左端点值 i 从1到n,每一个左端点值,对应的区间都有右端点值 j 从 i到n,每确定一对左右端点值,都要对该区间进行遍历求和。
#include <bits/stdc++.h>
using namespace std;
#define Long long long
int main() {
int n; cin >> n;
vector<int> v(n+1);
for (int i = 1; i <= n; i++) {
cin >> v[i];
}
Long max = v[1];
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
Long sum = 0;
int left = i;
while (left <= j)
sum += v[left++];
max = sum > max ? sum : max;
}
}
cout << max << endl;
return 0;
}
时间复杂度:=
即O(n³)
解法二:前缀和求解
只要前i-1项和为正数(>0),那么就继续求前 i 项和。一旦前i-1项和为负数,就从第i项重新开始往后求和,因为加上前i-1项的和(<0)只会减小区间的和结果,这时候只需要将前i-1项当作0,不予理会就行。后续计算的就是前i-1个0和第i个到第j个的和,如果还有小于0的情况,那么前j-1项全都是0,从j开始重新累加,以此类推。代码如下:(涉及一点点动态规划,要好好在纸上写写画画)
#include <bits/stdc++.h>
using namespace std;
/*最大子段和*/
int main()
{
int n; cin >> n;
vector<int> v(n + 1);
v[0]=0;
int ans = v[1];
for (int i = 1; i <= n; i++) {
cin >> v[i];
v[i] = max(v[i] + v[i - 1],v[i]);
if (ans < v[i]) ans = v[i];
}
cout << ans;
return 0;
}
第三题:最大子矩阵
解法一:暴力求解
暴力解法就是依次循环求出子矩阵的和,求出最大的子矩阵区间和。第一步,确定子矩阵左上角的点的坐标(start_i,start_j两层循环),第二步,确定子矩阵右下角的坐标(end_i,end_j两层循环),第三步,对子矩阵区间内的所有元素进行求和(i,j两层循环)。综上:六层循环遍历,时间复杂度O()。这绝对过不了啊。但代码还是给大家写一下:
#include <bits/stdc++.h>
using namespace std;
#define Long long long
const int N = 1e3 + 10;
Long a[N][N];
int cnt;
int main() {
Long n, m, k; cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
for (int start_i = 1; start_i <= n; start_i++) {
for (int start_j = 1; start_i <= m; start_j++) {
for (int end_i = start_i; end_i <= n; end_i++) {
for (int end_j = start_j; end_j <= m; end_j++)
{
Long sum = 0;
for (int i = start_i; i <= end_i; i++) {
for (int j = start_j; j <= end_j; j++) {
sum += a[i][j];
}
}
if (sum <= k)cnt++;
}
}
}
}
cout << cnt;
return 0;
}
解法二:一维前缀和
#include <bits/stdc++.h>
using namespace std;
#define Long long long
const int N = 1e3 + 10;
Long a[N][N], s[N][N];
int cnt;
int main() {
Long n, m, k; cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
s[i][j] = s[i][j - 1] + a[i][j];
}
}
for (int start_i = 1; start_i <= n; start_i++) {
for (int start_j = 1; start_i <= m; start_j++) {
for (int end_i = start_i; end_i <= n; end_i++) {
for (int end_j = start_j; end_j <= m; end_j++) {
Long sum = 0;
for (int i = start_i; i <= end_i; i++) {
sum += s[i][end_j] - s[i][start_j - 1];
}
if (sum <= k)cnt++;
}
}
}
}
cout << cnt;
return 0;
}
看循环,五层啊,还是过不了,怎么办?有一维前缀和,那我们能不能有二维前缀和呢?显然是的。
解法三:二维前缀和
#include <bits/stdc++.h>
using namespace std;
#define Long long long
int cnt;
int main()
{
int n, m; cin >> n >> m;
Long k; cin >> k;
vector<vector<int>> v(n+1, vector<int>(m+1));
vector<vector<Long>> s(n+1, vector<Long>(m+1));//二维前缀和
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> v[i][j];
s[i][j] = v[i][j] + s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1];//二维前缀和
}
}
for (int starti = 1; starti <= n; starti++) {
for (int startj = 1; startj <= m; startj++) {
for (int endi = starti; endi <= n; endi++) {
for (int endj = startj; endj <= m; endj++) {
Long sum = s[endi][endj] + s[starti - 1][startj - 1] - s[starti - 1][endj] - s[endi][startj - 1];
if (sum <= k)cnt++;
}
}
}
}
cout << cnt;
return 0;
}
时间复杂度来到了O() :但还是不能全部AC。这很正常,这道题需要用到的算法是:动态规划
课后题:前缀和与后缀和
课后总结:
一维前缀和公式
s[i]=a[i]+s[i-1]
二维前缀和公式
s[i][j] = a[i][j] + s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1]
算法这玩意,多思考、思考、思考!多练多练多练!