基础算法--前缀和

算法介绍:

前缀和算法是一种用空间换事件的算法,常用于解决某些题目或作为高级算法的组成部分。它可以用于快速计算数组元素之和,通过预先计算数组中每个位置前所有元素的累加和,将这些部分和存储在一个新数组中,从而在需要计算某个区间的和时,可以通过简单的减法操作得到结果,而不必重新遍历整个区间。

第一题:求区间和

P8218 【深进1.例1】求区间和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)icon-default.png?t=N7T8https://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)icon-default.png?t=N7T8https://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;
}

时间复杂度:\sum i^{2}=\frac{(i+1)*(i+2)*(2i+3)}{6} 即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;
}

 第三题:最大子矩阵

1.统计子矩阵 - 蓝桥云课 (lanqiao.cn)icon-default.png?t=N7T8https://www.lanqiao.cn/problems/2109/learning/?page=1&first_category_id=1&second_category_id=3&name=%E7%BB%9F%E8%AE%A1%E5%AD%90%E7%9F%A9%E9%98%B5

解法一:暴力求解

暴力解法就是依次循环求出子矩阵的和,求出最大的子矩阵区间和。第一步,确定子矩阵左上角的点的坐标(start_i,start_j两层循环),第二步,确定子矩阵右下角的坐标(end_i,end_j两层循环),第三步,对子矩阵区间内的所有元素进行求和(i,j两层循环)。综上:六层循环遍历,时间复杂度O(n^{6})。这绝对过不了啊。但代码还是给大家写一下:

#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(n^{4}) :但还是不能全部AC。这很正常,这道题需要用到的算法是:动态规划

课后题:前缀和与后缀和

724. 寻找数组的中心下标 - 力扣(LeetCode)


课后总结:

一维前缀和公式

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]


算法这玩意,多思考、思考、思考!多练多练多练!

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值