目录
58.区间和
文档讲解:代码随想录58.区间和
题目:58. 区间和(第九期模拟笔试) (kamacoder.com)
学习:本题最直接的做法是,将数组Array保存好后,通过下标遍历数组,以此来计算区间的总和。代码也很直观:保存数组,计算总和即可
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++) cin >> vec[i];
while (cin >> a >> b) {
int sum = 0;
// 累加区间 a 到 b 的和
for (int i = a; i <= b; i++) sum += vec[i];
cout << sum << endl;
}
}
但是这种方法,在本题中会存在超时的情况,因为这种解法,每次我们都需要遍历数组上下标,来计算区间的总和。假如存在一个极端的情况,我们查询m次,每次查询的范围都是0到n - 1,则该算法的时间复杂度为O(m * n)其中m为查询的次数,可见时间复杂度是很高的。
因此我们可以采取另一种方法来解决本题:前缀和。
前缀和在涉及计算区间和的问题时非常有用。前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。
其实很好理解,我们通过保存每个下标i(包括i)之前元素的累加和,就能够很容易得得到,某个区间的总和,例如:
此时如果我们想要统计数组下标1,3之间的累加和,我们只需要使用p[3] - p[0]即可。这是因为p[3]包含了下标3之前元素的和,而p[0]包含了下标0之前元素的和。这样一减,剩下的就是下标1到下标3之间的元素的和了。
又比如下标2和下标5之间,就是p[5] - p[1]:
p[1] = vec[0] + vec[1];
p[5] = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];
p[5] - p[1] = vec[2] + vec[3] + vec[4] + vec[5];
这样的话,我们再处理好前缀和数组后,每一次查找两个下标之间的区间和,时间复杂度就为O(1),极大的降低了时间复杂度。
这里要注意两点:
- 我们在进行前缀和减的时候是p[b] - p[a - 1]而不是p[a],这是因为我们需要下标a的值,区间是左闭右闭的。
- 又因为上述的原因,当a = 0时,我们需要单独处理情况,不需要减了,p[b]就是我们需要的值。
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> Arraysum(n); //记录前缀和
int ans;
int sum = 0;
for(int i = 0; i < n; i++) {
cin >> ans;
sum += ans;
Arraysum[i] = sum;
}
while(cin >> a >> b) {
int presum;
if(a == 0) {
presum = Arraysum[b];
}
else {
presum = Arraysum[b] - Arraysum[a - 1];
}
cout << presum << endl;
}
return 0;
}
C++代码,面对大量数据读取输出操作的时候,可以使用scanf 和 printf,耗时会减少很多:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n);
vector<int> p(n);
int presum = 0;
for (int i = 0; i < n; i++) {
scanf("%d", &vec[i]);
presum += vec[i];
p[i] = presum;
}
while (~scanf("%d%d", &a, &b)) {
int sum;
if (a == 0) sum = p[b];
else sum = p[b] - p[a - 1];
printf("%d\n", sum);
}
}
44. 开发商购买土地
文档讲解:代码随想录44.开发商购买土地
题目: 44. 开发商购买土地(第五期模拟笔试) (kamacoder.com)
学习:本题是一个分割问题,关键是只能纵向或者横向切割,并且根据题意只能切割一刀。本题存在暴力的解法,时间复杂度是O(n^3),使用一个for枚举分割线,嵌套两个for去累加区间里的和进行求解。
但我们可以根据上题的前缀和的思想,来进行时间复杂度的优化。
首先根据本题,我们只能纵向或者横向的切一刀,这就是两种情况,分别对应行分割和列分割。
1.行分割:对于行分割来说,我们可以理解为切一刀后,该位置之前的和就为A公司的区域和 sumA,而B公司的区域和为sumB = sum - sumA,而B公司和A公司的差值就为两者相减:
abs(sum - sumA - sumA)
可见其实我们只需要遍历A公司的区域和就可。因此我们先预处理数组,将其每一行的和求出来,再遍历行和数组,来确定A公司的区域和,以此来找到两个公司最小的差值。
result = min(result, abs(sum - sumA - sumA))
2.列分割:列分割同理,我们同样预处理数组,计算每一列的和,然后再遍历列和数组,来计算最小差值。
最终可以得到代码:时间复杂度为O(n^2)
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main () {
int n, m;
cin >> n >> m;
int sum = 0;
vector<vector<int>> vec(n, vector<int>(m, 0)) ;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
// 统计横向
vector<int> horizontal(n, 0);
for (int i = 0; i < n; i++) {
for (int j = 0 ; j < m; j++) {
horizontal[i] += vec[i][j];
}
}
// 统计纵向
vector<int> vertical(m , 0);
for (int j = 0; j < m; j++) {
for (int i = 0 ; i < n; i++) {
vertical[j] += vec[i][j];
}
}
int result = INT_MAX;
int horizontalCut = 0;
for (int i = 0 ; i < n; i++) {
horizontalCut += horizontal[i];
result = min(result, abs(sum - horizontalCut - horizontalCut));
}
int verticalCut = 0;
for (int j = 0; j < m; j++) {
verticalCut += vertical[j];
result = min(result, abs(sum - verticalCut - verticalCut));
}
cout << result << endl;
}
我们也可以进行一些优化: 实际上超过sum/2后,计算一次即可,后面差值只会越来越大
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> vec(n, vector<int>(m, 0));
int sum = 0; //统计总和
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
//统计行和
vector<int> linesum(n, 0);
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
linesum[i] += vec[i][j];
}
}
//统计列和
vector<int> colusum(m, 0);
for(int j = 0; j < m; j++) {
for(int i = 0; i < n; i++) {
colusum[j] += vec[i][j];
}
}
int result = INT_MAX;
int horisum = 0;
for(int i = 0; i < n; i++) {
horisum += linesum[i];
result = min(result, abs(sum - horisum - horisum));
if(horisum > sum/2) { //优化,实际上超过sum/2后,计算一次即可,后面差值只会越来越大
break;
}
}
int versum = 0;
for(int j = 0; j < m; j++) {
versum += colusum[j];
result = min(result, abs(sum - versum - versum));
if(versum > sum/2) { //优化,实际上超过sum/2后,计算一次即可,后面差值只会越来越大
break;
}
}
cout << result << endl;
return 0;
}
本题也可以在暴力求解的基础上,优化一下,就不用前缀和了,在行向遍历的时候,遇到行末尾就统一一下, 在列向遍历的时候,遇到列末尾就统计一下。时间复杂度也是O(n^2),空间复杂度稍微降低了一些。
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main () {
int n, m;
cin >> n >> m;
int sum = 0;
vector<vector<int>> vec(n, vector<int>(m, 0)) ;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
int result = INT_MAX;
int count = 0; // 统计遍历过的行
for (int i = 0; i < n; i++) {
for (int j = 0 ; j < m; j++) {
count += vec[i][j];
// 遍历到行末尾时候开始统计
if (j == m - 1) result = min (result, abs(sum - count - count));
}
}
count = 0; // 统计遍历过的列
for (int j = 0; j < m; j++) {
for (int i = 0 ; i < n; i++) {
count += vec[i][j];
// 遍历到列末尾的时候开始统计
if (i == n - 1) result = min (result, abs(sum - count - count));
}
}
cout << result << endl;
}
总结
数组的一些补充题型,前缀和的使用。实际上前缀和在后续动态规划中,同样也会被使用。