求和
(sum.cpp/c)
【问题描述】
有一个n个数的序列{a_i},对于k从1到n,求所有长度大于等于k的区间中前k大数的和的总和。
【输入格式】
输入文件名为sum.in。
第一行输入一个数n,表示给定的数的个数。
第二行输入n个整数,表示给定的序列。
【输出格式】
输出文件名为sum.out。
输出一行一个整数表示答案,最后答案对1e9+7取模。
【输入输出样例】
sum.in
sum.out
3
3 4 5
63
【样例解释】
给出各个区间的答案:
K=1:[1,1]=3,[1,2]=[2,2]=4,[1,3]=[2,3]=[3,3]=5
K=2:[1,2]=7,[2,3]=[1,3]=9
K=3:[1,3]=12
【数据规模与约定】
对于30% 的数据,满足n≤100。
对于60% 的数据,满足n≤1e3。
对于100% 的数据,满足n≤1e6,1≤a_i≤1e9。
先不考虑它的正解,用正常的思维去捋一遍计算答案的过程。
这道题可以变成这样:对于任意一个长度为l的区间,设它的起点是x,终点是y。
那么这个区间的数就是:Ax , Ax+1 , Ax+2 ......Ay-2 , Ay-1 , Ay
对它进行从小到大的排列后变成:Bx,Bx+1,Bx+2......By-2 , By-1 , By。
因为1<=k<=l,所以整个区间要加l次。
分别加:
By
(By)+(By-1)
(By)+(By-1)+(By-2)
......
(By)+(By-1)+(By-2)+......(Bx+2)
(By)+(By-1)+(By-2)+......(Bx+2)+(Bx+1)
(By)+(By-1)+(By-2)+......(Bx+2)+(Bx+1)+(Bx)
-------------------------------------------------------------
将整个式子竖着加起来,就会变成
By*l + (By-1)*(l-1) + (By-2)*(l-2)...... + Bx*1
换句话说也就是,每个数本身再乘上这个数在当前区间从小到大的排名之和
进而我们可以推论——只要我们找到某个数字在所有包含它的区间内的排名,将这些排名求和,再乘上它本身的数值,就是这个数字对整个答案的贡献。将每个数字都进行相同的操作,就可以求得最后的答案。
求排名的话我们不可能真的排一遍序,这样会超时的。所以这里需要一个特别重要的转化思维:求排名等效于求有多少个数比它小
假设我们找到了一个数字Aj,它要比当前的Ai要小,那么它将会在每一个同时包含Ai和Aj的区间内影响Ai的排名。
那这样的区间有多少个呢?(其中i和j都是坐标)
如果j在i的前面:有j*(n-i+1)个
如果j在i的后面:有i*(n-j+1)个
这也很容易理解,就是i,j两侧(包括i、j)的位置任意组合,就可以搭配成一个同时包含i和j的区间。
但是注意,求排名等效于求有多少个数字比它小,但是并不等同于求有多少个数字比它小。
比如前面如果没有数比它小,返回的结果是0,但是它的排名是1。所以,求排名还要算上自己。
但是这仍然是个N方做法,还需要进一步优化
观察一下式子:
如果j在i的前面:有j*(n-i+1)个
如果j在i的后面:有i*(n-j+1)个
因为i是固定的,所以第一个式子中的(n-i+1)是固定的,第二个式子中的 i 是固定的
所以我们只需要知道前面比ai小的数的j,和后面比ai小的数的n-j+1即可
从前往后的时候,开一个树状数组,下标是a数组离散化后对应的数字(也就是排名),记录的是这个排名上对于每一个j的j和
从后往前的时候,开一个树状数组,下标是a数组离散化后对应的数字(也就是排名),记录的是这个排名上对于每一个j的(n-j+1)和
所以从前往后时的操作是这样的:
得到当前数字的离散结果,将这个数插入树状数组(因为排名要算上自己),再问这个离散结果在树状数组对应的下标及其以前的值和,就得到了所有出现在i前面(以及i本身)的坐标值j和。
从后往前的操作时这样的:
得到当前数字的离散结果,不用将这个数插入树状数组(因为自己对自己的排名影响只用算1次)再问这个离散结果在树状数组对应的下标及其以前的值和,就得到了所有出现在i前面(以及i本身)的(n-i+1)和,最后再将这个数插入树状数组。
但是这里还有一个问题:就是可能会出现同样大小的数字,那这时候它的排名怎么算呢?
答:将它们视为不同大小的数字,毕竟我要求的是前k大,无论多少个数字相同我只要k个数字,离散的时候将它们变得不同,就会保证最后答案的正确性。或者还有一种办法,将它们视为同样大,但是从前往后和从后往前的时候就要注意——一个是问“离散结果在树状数组对应的下标及其以前的值和” 一个是问“(离散结果-1)在树状数组对应的下标及其以前的值和”,相当于一次说:先出现的、和我同样大的数字排名都要比我小,一次说:先出现的、和我同样大的数字排名都要比我大,因为一次是从前往后,一次是从后往前,所以这两句话就非常的协调、不存在任何矛盾。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
inline ll read()
{
char c=getchar();ll f=1,s=0;
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){s=s*10+c-'0';c=getchar();}
return f*s;
}
ll n,nn;
ll a[1000010];
ll b[1000010];
ll c[1000010];
ll sum[1000010];
ll d[1000010];
inline int find(ll x)
{
int l=1,r=nn,mid;
while(l<=r)
{
mid=(l+r)>>1;
if(x==c[mid])return mid;
else if(x<c[mid])r=mid-1;
else l=mid+1;
}
}
inline int lowbit(int x){return x&-x;}
inline void add(ll x,ll y)
{
while(x<=nn)
{
sum[x]+=y;
x+=lowbit(x);
}
}
inline ll getsum(ll x)
{
ll ans=0;
while(x)
{
ans+=sum[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
//freopen("sum10.in","r",stdin);
nn=n=read();
for(int i=1;i<=n;i++)a[i]=read(),c[i]=a[i];
sort(c+1,c+nn+1);
nn=unique(c+1,c+nn+1)-(c+1);
for(int i=1;i<=n;i++)b[i]=find(a[i]);
ll ans=0;
for(int i=1;i<=n;i++)
{
add(b[i],i);
ll s=getsum(b[i])*(n-i+1)%mod;
ans=(ans+(s*a[i])%mod)%mod;
}
memset(sum,0,sizeof(sum));
for(int i=n;i>=1;i--)
{
add(b[i],n-i+1);
ll s=getsum(b[i]-1)*i%mod;
ans=(ans+(s*a[i])%mod)%mod;
}
printf("%lld\n",ans);
return 0;
}
注意理解一个关键:getsum(b[i]-1)中的-1有两个作用,首先它可以排除两次累加自己的情况,从而不影响排名的正确性;其次,它可以问“(离散结果-1)在树状数组对应的下标及其以前的值和”,从而让相同的数字有了大小之分
总结:
先不考虑正解,用正常的思维去捋一遍计算答案的过程。
将一种问法,转化为另一种问法
求排名等效于求有多少个数比它小(当然还要算上自己,而且只能算一次)
若j<=i 则同时包含i和j的区间个数是j*(n-i+1)
求解时将固定不变的量提出来,只剩下变量,简化解法。
树状数组可以边求前缀和边进行修改,时间复杂度为logn