题目描述
给定一个长度为 $n$ 的整数数列 $a_1,a_2,…,a_n$ 和一个整数 $t$。
请你判断共有多少个数对 $(l,r)$ 同时满足:
$1≤l≤r≤n$
$a_{l}+a_{l+1}+…+a_{r−1}+a_{r}<t$
前言
本题真的非常的有意思,我也是被这题绊住了,用了大约40分钟才想出正解,压哨AC……
只能说技不如人吧QAQ,但是毕竟做出来了,那就给大家写一篇题解
分析
看到静态求和,可以联想到前缀和,于是我们先求出整个序列的前缀和数组$sum$。
这时脑中顿时蹦出一个想法:枚举右端点,然后二分出第一个满足条件的左端点。
二分的判断条件:$sum_r-sum_{l-1}<t$
可惜啦,由于有负数的存在,单调性并不满足,所以二分是假做法。
那怎么办呢?
我又得到了天启:可以用一个数据结构把前面的前缀和打散放进去,而不必拘泥于原序列的限制。
每个左端点 $l$ 满足条件当且仅当:
$sum_r-sum_{l-1}<t$
即:$sum_{l-1}>sum_r-t$
正解:在值域上维护一个树状数组,维护每个前面的前缀和数值的个数的和,枚举每个右端点$r$,每次将答案累加上树状数组中大于$sum_{r}-t$的总个数,然后再给 $sum_r$ 这个位置的个数加上$1$,方便后续的计算
由于值域很大很大,树状数组开不下,所以我们要先将前缀和数组离散化
AC代码:
#include<bits/stdc++.h>
#define int long long
#define lowbit (x&-x)
using namespace std;
const int N=1000010;
int n,m;
int s[N];
int tr[N];
vector<int> nums;
void add(int x,int k){ //树状数组修改操作
for(int i=x;i<=1000000;i+=lowbit(i)) tr[i]+=k;
}
int sum(int x){ //树状数组查询操作
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int get(int x){ //二分获取离散化之后的值
return lower_bound(nums.begin(),nums.end(),x)-nums.begin()+1;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
s[i]+=s[i-1];
}
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()); //离散化之判重
int res=0;
add(get(s[0]),1); //将 0 这个位置的前缀和加入树状数组
for(int i=1;i<=n;i++){
res+=sum(1000000)-sum(get(s[i]-m)); //答案累加上树状数组中大于sum[r]-t的总个数
add(get(s[i]),1); //给sum[r]这个位置的个数加上1,方便后续的计算
}
cout<<res;
return 0;
}
res+=sum(1000000)-sum(get(s[i]-m));
为什么是1000000呢
$sum(1000000)$表示的是整个值域上所有值的总个数,$sum(get(s[i]-m))$表示所有$i$之前的前缀和中小于等于$get(s[i]-m)$的前缀和的个数,二者相减就表示所有$i$之前的前缀和中大于$get(s[i]-m)$的前缀和的个数,类似于“整体减空白”的思想~~
是1000000而不是999999是因为我的树状数组开了这么大,其实取500000也是可以的
还是理解不了,可能是因为我没学过树状数组吧。
hh,那建议你先学一下吧,树状数组的本质其实就是动态支持修改的前缀和
请问一下 为什么需要初始树状数组中要0需要加1啊
因为我们考虑的不是左端点,而是左端点的前一个位置 $S_{l-1}$,这样也是为了方便做前缀和;初始时 $0$ 加上 $1$,意思就是把 $S_0$ 加入我们后续对“左端点的前一个位置”的考虑范围内
比如 $r$ 枚举到 $2$ 的时候,$S_2$ 减去 $S_0$ 就表示 $[1,2]$ 这个区间的和,如果不加入 $0$ 这个位置的话,所有 $1$ 开头的子段就考虑不到
但是树状数组维护的是这个数出现的次数(值域经过离散化得到的)啊,也就是已出现的值的次数,初始情况下应该一个也没有出现吧
佬,我自己思考了一个解释方法,就是考虑三种情况
Si>m
Si==m
Si<m
,第一种情况Si-m>0并且Si这个位置本身不符合,第二种情况Si==m,Si这个位置本身也不符合,但是他做统计的时候是减去[0, i)不应该把自己包含进去,第三种情况i这个位置符合需要把自己包含进去什么是天启
上天给我们的启迪(滑稽
6
hhh
sum[r]这个位置的个数为什么不是减一啊
这枚举的是左端点吧
枚举的就是右端点。求前缀和的时候,右端点不减$1$,左端点减$1$
i不是从0开始的嘛
我好像懂了,枚举的是右指针吧
对滴
每次都对左端点排序的话,那还不如$O(n^2)$的暴力做法快;如果用$vector$每次动态插入新值的话,由于$vector$不是链表,二分出来位置之后插入的过程还是$O(n)$的,你这个想法倒是可以用$set$实现,不过$set$不支持$getk$和$rank$操作,所以只能手写平衡树……最后你会发现还是树状数组最香,嘿嘿
以上回复 曙光-晨曦
懂了懂了,谢谢大佬!主要是还没学树状数组
嘻嘻,一起加油吧φ(>ω<*)
请问一下为啥有负数存在不能二分呢
不可以在枚举右端点的时候,对左端点进行排序,然后二分吗。当然,这应该是会t的,但如果使用vector配合upperbound,就可以省掉排序的过程,时间复杂度应该是nlogn,但是我t了 呜呜。请问我这个思路有问题吗
有负数的话,$1$到$i-1$的前缀和就不再是单调递增的,而我们希望查找出第一个大于$sum_r−t$的位置,必须借助前缀和单调递增的性质
什么时候才能这么强
多听 $y$ 总讲课,多刷题,每个人都要经历不断进步的过程,高智商和强天赋固然有优势,但是后天的努力也同样必不可少
过了这么多月,回过头来看确实成长了不少了!!
当时自己赛时切了这个题那种成就感简直爆棚,现在回来看看发现自己当初确实有点幼稚哈,我们机房每个人估计都能秒切QAQ,确实发现这段时间大家进步都飞快!!!
牛的!一起加油吧!
没开long long 吧
比赛的时候开了,一开始交题解的时候为了增加可读性就没开,现在已经开 $long long$ 啦,谢谢提醒!