题意:

n=1,2时,a[n] =1;

n>=3时,a[n] = a[n-a[n-1]] + a[n-1-a[n-2]];

现在给定一个n(1<=n<=1e18),求前a的前n项和,结果mod(1e9+7)

思路:

大体思路:找规律->求出a[n]->求出a的前n项和

详细思路:

打表打出a后发现序列是这样的 1 1 2 2 3 4 4 4 5 6 6 7 8 8 8 8 9 10 10 ...

我们把每个数值的出现次数打出来,得到:

数值:        1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

出现次数:2 2 1 3 1 2 1 4 1  2   1   3   1   2   1   5...

1的出现次数破坏了我们接下来要讲的规律,所以我们将a[1] = 1拿出来单独讨论,找剩余元素的规律

再次将每个数值的出现次数打出来,得到:

数值:        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 ^ i个元素其实就是把前2^(i-1)个元素复制两次,然后把第2^i个元素的值加1

这样我们就可以得到元素出现次数上的规律了,设cnt[i]为前2^i个元素出现次数之和,那么cnt[i] = 2 * cnt[i-1] + 1

我们知道任何一个数都可以表示成若干个2的幂次方之和,那么给定一个下标n,我们便可以通过cnt求出a[n],具体做法如下:

    ll a = 0;
for (int i = 62; i >= 0; i--) {
while (cnt[i] <= n) {
n -= cnt[i];
a += (1LL<<i);
}
}

这里我们顺便求一下a[n]这个值在a序列中第一次出现的位置,原理同上,这个之后要用到

    ll pos = 0;
for (int i = 62; i >= 0; i--) {
while ((1LL<<i) <= a) {
a -= (1LL<<i);
pos += cnt[i];
}
}
return pos + 1;//这里+1是因为规律是从第二项开始的

下面就可以求和了,再次观察每个数值的出现次数:

数值:        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...

我们发现1,3,5,7,9,11,13,15...出现了1次

                2,6,10,14...出现了2次

                4,12...出现了3次

打表打下去就会发现数值为奇数*1的数都出现了1次,数值为奇数*2的数都出现了两次,数值为奇数*3的数都出现了3次...

这就提醒我们枚举数值的出现次数i,i对应的数值序列实际上是一个等差数列,比如2对应的2,6,10,14...就是一个等差数列,这个等差数列的首项是2^(i-1),公差为2^i,那么我们只要知道等差数列的项数就可以求和了。

之前我们求得了a[n],那么我们便可以通过a[n]确定等差数列的项数。我们设等差数列的尾项为an,则有an<=a[n],这样我们便可以用等差数列的通项公式求出an,然后便可以知道有多少项了。

之后我们就可以对每个等差数列求和,然后乘上相应的出现次数i,理论上就可以得到结果了,但是还没完,因为等差数列中的尾项不一定出现i次,举个例子,n=10,那么可以求出a[10] = 6,出现2次的数值理论上有2,6,但是仔细看一下对于n=10,6这个数值是不是只出现了1次?这就提醒我们对于边界要特判,现在就可以考虑一下我之前为什么要求a[n]这个值在a序列中第一次出现的位置了,具体做法说起来比较乱,看代码吧,其实也不用非得像我这么做。

最后再次强调一下取模,能取模的地方尽量取!

到此就解决了这道题

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

typedef long long ll;
const ll mod = 1e9 + 7;

ll cala(ll n) {//给定n,计算a[n]
//因为规律是从第二项开始的,所以一开始先特判一下
if (n == 1) return 1;
n--;

ll cnt[64];//cnt[i]表示数值1~2^i每项的出现次数之和
cnt[0] = 1;
for (int i = 1; i <= 62; i++) cnt[i] = 2*cnt[i-1] + 1;

ll a = 0;
for (int i = 62; i >= 0; i--) {
while (cnt[i] <= n) {
n -= cnt[i];
a += (1LL<<i);
}
}
return a;
}

ll posa(ll a) {//求a[n]这个值在a序列中第一次出现的位置
//同样先特判一下
if (a == 0) return 1;

ll cnt[64];//cnt[i]表示数值1~2^i每项的出现次数之和
cnt[0] = 1;
for (int i = 1; i <= 62; i++) cnt[i] = 2*cnt[i-1] + 1;

ll pos = 0;
for (int i = 62; i >= 0; i--) {
while ((1LL<<i) <= a) {
a -= (1LL<<i);
pos += cnt[i];
}
}
return pos + 1;//这里+1是因为规律是从第二项开始的
}

int main() {
int T;
ll N;
scanf("%d", &T);
while (T--) {
scanf("%lld", &N);
ll a = cala(N);//求a[n]
ll cnt = N - posa(a-1);//cnt为a[n]在序列中出现的次数
ll ans = 0;
for (int i = 1; (1LL<<(i-1)) <= a; i++) {
//计算等差数列首项,公差,项数,尾项
ll a1 = (1LL<<(i-1));
ll d = (1LL<<i);
ll n = ((a - a1) % d == 0 ? (a-a1)/d : (a-a1)/d+1);//注意一下这个判断
ll an = a1 + d * (n-1);
//求和,用了一下费马小定理
ll sum = (((a1%mod + an%mod)%mod) * (n%mod)) % mod * 500000004 % mod;
ans = (ans%mod + (sum%mod*i%mod)%mod) % mod;
if ((a-a1) % d == 0) ans = (ans%mod + (cnt%mod*a%mod)%mod) % mod;//注意一下这个判断
}
printf("%lld\n", ans + 1);//规律是从第二项开始的,所以ans+1
}
return 0;
}