树状数组 + 离散化 + 前缀和
通过数据范围推断复杂度应该是O(nogn)
看到静态求和,可以联想到前缀和,于是我们先求出整个序列的前缀和数组sum。
这时脑中顿时蹦出一个想法:枚举右端点,然后二分出第一个满足条件的左端点。
二分的判断条件:sumr−suml−1<t
可惜啦,由于有负数的存在,单调性并不满足,所以二分是假做法。怎么办呢?
我们可以重新审视满足答案要求的公式:
sumr − sum(l−1) < t
我们可以发现对于每一个枚举的右端点R,sumr和t的值是恒定的,不会改变,那么我们能不能用一个数组p来记录每一个sumL的值的个数,通过前缀和的方式得出满足条件的L的个数。很显然,是不可以的,或者说不能直接通过前缀和实现,因为在我们枚举到一个右端点R的时候,数组p中不能端点R后面的值,因为L必须在R的左侧,否则就会有重复,所以说这个数组p是动态变化的,只有当前端点R已经遍历过了,才可以在数组p中加入sumr,作为下一个端点R的sumL,因此前缀和也是动态变化的。但树状数组和线段树可以解决此类动态求前缀和的问题。
正解:在值域上维护一个树状数组,维护前缀和数值的个数的和,枚举每个右端点r,每次将答案累加上树状数组中大于sumr−t的总个数,然后再给sumr 这个位置的个数加上1,方便后续的计算
由于值域很大很大,树状数组开不下,所以我们要先将前缀和数组离散化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 400010;
typedef long long LL;
int n, tr[N];
LL m, s[N];
vector<LL> nums;
int get(LL x)
{
return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;
//+1是确保梳状数字下标从1开始
}
int lowbit(int x)
{
return x & -x;
}
void add(int x, int v)//树状数组修改操作
{
for(int i = x; i < N; i += lowbit(i))
tr[i] += v;
}
int query(int x)//树状数组查询操作
{
int res = 0;
for(int i = x; i ; i -= lowbit(i))
res += tr[i];
return res;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
{
LL x; cin >> x;
s[i] = s[i - 1] + x;
}
for(int i = 0; i <= n; i ++ )//将所有前缀和以及后续询问用到的数值全部离散化
{
nums.push_back(s[i]);
nums.push_back(s[i] - m);
}
sort(nums.begin(), nums.end());//离散化之前一定要线排序
nums.erase(unique(nums.begin(), nums.end()), nums.end());//离散化之去重
LL res = 0;//结果可能爆int
//对于每一个R,L可以取得的范围是1~R,那么对应与前缀和下标(L-1)就是0~R-1
//所以在R=1之前我们要把L=0时的值s[0]加入里面
add(get(s[0]), 1);
for(int i = 1; i <= n; i ++ )
{
//大于s[i]-m的个数就是总个数减去小于等于s[i]-m的个数
res += i - query(get(s[i] - m));
add(get(s[i]), 1);//加入到前缀和当中
}
cout << res << endl;
return 0;
}