题目描述
知识点
思维
实现
码前思考
- 求一个数组的所有子数组的和,可以通过前缀和的减法得到! 这样的时间复杂度为 ,不必要枚举所有子数组然后再一一求和,那样的时间复杂度为 了!
- 一个子数组是有左右边界的,这道题目得使用 枚举右边界 的方法来解题,即统计以当前数字为有边界,能够得到的所有子数组!
- 大神的解题思路是这样的:
只枚举子数组右界来计算对答案的贡献。
可以发现,若某个左界使得当前子数组不是很6的,则包含当前子数组的子数组也不是很6的。
所以我们可以找到最近的不是很6的左界,然后这个左界右边的左界对于当前右界都是很6的。
于是问题转化为为每个右界寻找最近的不是很6的左界,这里可以用 或者 (map
的查找时间复杂度为 ,unordered_map
为 ) 的复杂度做到。比如用一个映射,记录前缀和为 的最右位置。
当然要注意对于当前右界找到的左界不能越过前面的右界找到的左界,否则也是包含了不是很6的子数组。(这几句话有点绕,推荐直接看代码,应该比较好懂)
代码实现
#include "bits/stdc++.h"
using namespace std;
typedef long long ll;
//pos表示当前前缀和为key的最近位置value
//使用ll是因为前缀和可能会超出int类型
unordered_map<ll,int> pos;
int main(){
int t;
scanf("%d",&t);
while(t--){
//输入的数组的大小
int n;
scanf("%d",&n);
//前缀和
ll s = 0;
pos[0] = 0; //初始化的时候,长度为0的前缀和是0
int r = 0; //当前读入的最右的左界限
int res = 0;
//读入数组中的每一个数字进行处理
int a;
for(int i=1;i<=n;i++){
scanf("%d",&a);
s += a;
if(pos.count(s)){ //如果前面的前缀和存在s,说明该pos[s]和i之间为0
//但是同时也要考虑pos[s]~i之间是否还有0
if(pos[s] >= r){ //说明pos[s]~i之间没有0
res += i - pos[s] - 1;
r = pos[s] + 1;
}else{ //如果pos[s]~i之间有0的子数组,那么r一定是大于pos[s]的
res += i - r;
}
}else{ //不存在这个前缀和,则直接找最近的那个不6的数组
res += i - r;
}
pos[s] = i; //每次都要更新
}
printf("%d\n",res);
//注意清理共享数据结构
pos.clear();
}
return 0;
}
码后反思
- 使用
ll
是因为前缀和可能会超出int
类型; - 注意共享数据结构要每次进行清空才对,例如上面代码中的
pos.clear()
; - 算法的主要思想就两个:
① 自左到右,记录前缀和;
② 更新当前字符串的最右的左边界。 - 最大的收获应该就是知道 求一个数组的所有子数组的和,可以通过前缀和的减法得到!,我之前还用了动态规划来解题,但是还是这种思路的转化问题视角更好!