http://acm.hdu.edu.cn/showproblem.php?pid=6304
题意:给出一个数列的递推式,求前n项和。
(因为图片加载不上,递推式自己去HDU6304看吧)
思路:这个题的n非常大(预处理不现实,所以先找规律吧),并且查询的组数T<=1e5,(T非常大)所以一定是一个log级别的查询
我的思路跟题解可能不太一样,我下面写的是我比赛时的具体思考过程。
打表:
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
int a[1050];
int main()
{
a[1]=a[2]=1;
for(int i=2;i<100;i++)
{
a[i]=a[i-a[i-1]]+a[i-1-a[i-2]];
printf("%d\n",a[i]);
}
}
1 1 2 2 3 4 4 4 5 6 6 7 8 8 8 8 9 10 10 11 12 12 12 13 14 14 15 16 16 16 16 16
看这个规律,你会发现所有的奇数都是出现了1次(除了1),那么先不管奇数,只看偶数
2 2 4 4 4 6 6 8 8 8 8 10 10 12 12 12 14 14 16 16 16 16 16 18 18 20 20 20 22 22 24 24 24 24 26 26 28 28 28 30 30 32 32 32 32 32 32 34 34 36 36 36 38 38 40 40 40 40
现在只看每个数字出现的个数(***数字出现的个数***)
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 6 (这些是到32的规律)
然后我猜想后面的规律为:2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 7
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 6
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 8
看到a[256]=128,并且128是出现了8次,完全跟我猜的次数一样。那么我就知道规律了
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 6
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 7
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 6
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 8
将上面4行从中间分成两份,只有最后一个数字多了一个1,其他数字都一样
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 6
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 7
前两行也符合。
2 3 2 4 2 3 2 5 2 3 2 4 2 3 2 6
第一行也符合
2 3 2 4 2 3 2 5
一直这样分下去
2 3 2 4
2 3
还记得当时省略的那些 1 吗?
(因为最前面的那个1不符合规律,所以先将那个1忽略了,然后前几项如下所示):
1 2 2 3 4 4 4 5 6 6 7 8 8 8 8 9 10 10 11 12 12 12 13 14 14 15 16 16 16 16 16
---> 数字大小:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
---> 出现次数:1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 5 (上面的数字出现了多少次,也正是上面的那个数列了)
这里一定要先理清楚,再看下面。
刚才说了这些都是有那个规律的 2 3 2 4 2 3 2 5
现在我们把1加上, 1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 5同样符合那个规律
1 2 1 3 1 2 1 4 (中间的空格代表分成了两部分,后一部分的最后一个数字,比前面的那个多1)
1 2 1 3
1 2
这个规律很明显,这个只是第一步,找到了规律,现在需要将规律总结成公式
前几项和s[ i ] : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
---> 数字大小:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
---> 出现次数:1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 5
s[1]= 1*1=1;
s[2]=s[1]+2*2=5;
s[3]=s[2]+3*1=8;
s[4]=s[3]+4*3=20;
这样推下去的话s数组肯定是开不下的,因为n<=1e18.所以考虑倍增思想。
我用s[0]表示前2^0项,s[i]表示前2^i项的总和。
那么我们就可以重新写出s数组的值了
s[0]=1;
s[1]=5;
s[2]=20;
s[3]=.....;
现在我们不能用数列来求s[ ]数组,应该利用规律,将s[i]=s[i-1]+ F( i ) ;
现在应该思考s[i]和s[i-1]有什么关系。
---> 数字大小:1 2 3 4 5 6 7 8
---> 出现次数:1 2 1 3 1 2 1 4
(我将s[2]简写成s2了)假设已知s2=1*1+2*2+3*1+4*3;
s3=s2+ 5*1+6*2+7*1+8*4 ;
1*1+2*2+3*1+4*3
5*1+6*2+7*1+8*4
观察黑体数字,这个规律再上面已经证明过了,只有最后一个数字多了1,其他都一样,那么我们最后再算那个多出来的数字
1*1+2*2+3*1+4*3
5*1+6*2+7*1+8*3 +8(这个8最后再算,先看前面这个式子怎么求)
用下面的这个式子减去上面这个式子得
4*1+4*2+4*1+4*3,是不是发现前面的那个差是一样的,(将差(4)提出来)
4*(1+2+1+3);
所以现在s3=s2+ (s2+4*(1+2+1+3)+8);这个式子很关键一定要看懂
---> 出现次数:1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 5
这个数列还记得吧,因为求s3需要这个式子的前几项和,所以我定义a[i]为次数的前2^i项和(跟s数组的定义是一样的)
那么a0=1;
a1=3;
a2=7;
a3=15;
a数组是不是一眼就看出了规律。a[i]=2^(i+1) -1;
s3=s2+ (s2+4*(1+2+1+3)+8);
那么这个式子是不是可以化简成s3=2*s2 + 4*a2 + 8 ;
现在再思考4和8又是什么。4是差值,也就是两段数字的差值了,也就是前面一段的长度4,这个应该也很好理解
那8呢?是你所求的最后一个数字,并且这个数字一定是2的次幂,8=2^3
所以s3=2*s2+2^2*a2+2^3;
将这个式子转换成sn
及sn=2*s(n-1)+2^(n-1)*a(n-1)+2^n;(这些sn和an都是可以直接预处理出来的)(求到s61就够用了)
并且an=2^(n+1)-1;
到这里题目就已经解决了一半了。
现在我们自己已经总结出来的规律的公式。现在要想的是怎么将题目要求的前n项转化为我们已知的sn和an。(想办法往公式上套就行了)
重点来了:现在求n=100的答案;
我们的sn表示的都是规律的前2^n项,那么我们要把100转化成2的次幂的形式
---> 数字大小:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
---> 出现次数:1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 5
现在再看这个数列的规律,我们先理一下思路,因为这里有个比较绕的东西。
首先前n=100项的和是不是包含第一项1,我们的规律是不包含最前面的1的,所以1最后单独处理
所以也就是求我们规律的前99项,对吧
现在理清楚前99项对于规律到底是个啥东西。
首先a数组表示的是数字的个数,a0=1表示规律的前一项。a1=3表示规律的前五项,a2=7表示规律的前七项,
an=2^(n+1)-1;
sn对应的是an,
所以将99对应an=2^(n+1)-1;才能对应sn(an对应的所有项的答案就是sn)
所以要将99转化为2的幂次-1,才行。
99=63+31+3+1+1,转化完之后再分别求即可。99相当于分成了5段,第一段长度为63,第二段长度为31,以此类推
第一段长度为63怎么求,
a5=2^(5+1) -1=63; 所以63对应的是a5,对应的答案的贡献是s5,所以前63项就求出来了,就是s5.
接着求第二段长度为31,(这里是难点)这个求法跟前面推sn的递推式是一样的。
这段的31个数字跟最前面的31个数字出现的次数是一一对应相等的,只是数字大小不同,而且数字大小差值,是能求出来的是32
做差为32*(a5)
为了能讲清楚,我把第一段前63项打出来:1 2 2 3 4 4 4 5 6 6 7 8 8 8 8 9 10 10 11 12 12 12 13 14 14 15 16 16 16 16 16 17 18 18 19 20 20 20 21 22 22 23 24 24 24 24 25 26 26 27 28 28 28 29 30 30 31 32 32 32 32 32 32
第二段长度31:33 34 34 35 36 36 36 37 38 38 39 40 40 40 40 41 42 42 43 44 44 44 45 46 46 47 48 48 48 48 48
请注意:1 2 2 3 4 4 4 5 6 6 7 8 8 8 8 9 10 10 11 12 12 12 13 14 14 15 16 16 16 16 16的和是不是s4(s4表示前a4=2^5-1项的和)
第二段每个数字比s4表示的数字都多了32,那么第二段的和就等于s4+32*a4.比s4总共多了a4个32;
这点比较难理解,剩下的就好办了(看不懂这里的再回去看一下我是怎么推出sn的,方法是一样的,利用的也是差值)
剩下的第三段跟第二段计算方法一样,第三段的和= s1+(32+16)*a1;
第四段的和= s0+(32+16+2)*a0;
第五段的和= s0+(32+16+2+1)*a0;
所以前100项的和就等于s5+s4+32*a4+s1+(32+16)*a1+s0+(32+16+2)*a0+s0+(32+16+2+1)*a0;
附上比赛时的AC代码:
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef long long LL;
const LL mod=1e9+7;
LL s[105],a[105],fac[105];//fac[i]=2^i,s和a数组跟博客里讲的一样
void init()//预处理
{
fac[0]=1;
s[0]=1;
a[0]=1;
for(LL i=1;i<=61;i++)fac[i]=fac[i-1]*2;
for(LL i=1;i<=61;i++)a[i]=(fac[i+1]-1+mod)%mod;
for(LL i=1;i<=61;i++)s[i]=(s[i-1]*2%mod+(fac[i-1]%mod)*a[i-1]%mod+fac[i])%mod;
}
LL query(LL x)
{
LL y=x,ans=0,t=0;//t来存储差值
for(LL i=61;i>=1;i--)
{
while(y>=fac[i]-1)
{
ans=(ans+s[i-1]+t*(a[i-1]))%mod;
t=(t+fac[i-1])%mod;//更新差值
y-=fac[i]-1;
}
}
return (ans+1)%mod;//+1是加上第一项的1
}
int main()
{
init();
LL t,n;
scanf("%lld",&t);
while(t--)
{
scanf("%lld",&n);
printf("%lld\n",query(n-1));//我们推得规律是不包含第一项的,所以n先-1。最后加上即可
}
return 0;
}