【阶段2】【学校测试】【树状数组】【思维转化】求和

求和

(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值