题目大意:求将正整数x分解为若干个互不相同的斐波那契数列中的数的和的方案数,x小于等于1e18,共n组数据,n不超过十万。
思路过程:计算出100以内的斐波那契数列,发现在第88项以后的数字大于1e18,所以只用考虑数列前88项,刚开始想到用map记忆化搜索,不出意外地超时了,然后考虑dp,考虑某一位选与不选, 发现跟记忆化搜索复杂度差不多,而且空间开不下,然后就卡住了,发现没有使用斐波那契数列的性质,就是第i项等于第i - 1项与第i - 2项之和, 这个性质有什么用呢?我们假设我们已经有了一种选数方案,假设我们要得到13,现在有一种选数方案为5, 8,考虑怎么用这种方案找到别的方案,考虑5,由于5等于它的前两项2, 3之和,那么我们可以不选5,选2,3,这样我们得到了一种新方案2,3,8。我们假设这种找到别的方案的方法叫“扩张”,每扩张一次选的数的数目就多了一个,比如5, 8 是两个数扩张一次成了2,3,8就是三个数,所以我们猜想数字多的方案一定是由数字少的方案通过若干次扩张得来的,这个我不会证,还有个猜想就是数字最少的方案是从大到小的贪心选取斐波那契数列,感性理解就是选的数字越大,需要的数字越少,这个我也不会证,我们假设称这个选择最少数字的方案选择了的数字集合从小到大排列为S。有了这两个猜想那就能做这道题了。
在此之前还要算一个东西, 假设 i < j, 我们要从第j项扩张到第i项有多少种方案,举个例子:假设我们要合成14,现在S为1, 13,那么13 可以扩张为5, 8.继续扩张5,得到2, 3。然后回到5, 8 的情况考虑能不能扩张8,假设扩张8,那就会得到3,5,5,我们发现多了个5,那就必须扩张5,那就会得到2, 3, 3, 5,发现多了个3,必须扩张3,那就得到1,2,2,3,5,结果卡住了,因为2不能分为1,1。题目要求数字各不相同。所以我们猜想每次扩张的后一位不能再继续扩张,说起来有点抽象,实际就是上面例子中13扩张为5,8我们猜想8不能继续扩张,5扩张为2,3我们猜想3不能继续扩张,也就是扩张出来的大的那个不能继续扩张。证明,我们设当前位为第i位,一次扩张得到第i - 2位与i - 1位,若继续扩张i - 1位,就会得到2 个 i - 2位与一个i - 3位,所以必须扩张一个i - 2,又会得到两个i - 3位,以此类推会得到两个2然后不能再扩张了,就卡住了,所以得证。所以每次扩张只能扩张最小的那个数字,所以方案数为(j - i) / 2.
设 dp[ i ][0 / 1] 代表S中第i个数是否被选择,那可以写出状态转移方程:
dp[ i ][ 1 ] = dp[i - 1][1] + dp[i - 1][0];
这代表第i个数被选择了,所以第i位不扩张,方案数就是前i - 1 位的方案数
dp[ i ][ 0 ] = dp[i - 1][1] * (pos[i] - pos[i - 1] - 1) / 2 + dp[i - 1][0] * (pos[i] - pos[i - 1]) / 2;
首先pos[i]代表S中第i个数在原斐波那契数列的位置,如果第i - 1位被选择了那就只能只能扩张pos[i] - pos[i - 1] - 1位, 否则没被选择表示第i - 1位被扩张了,根据我们上面证明它扩张出的的第i - 2位是不能用于扩张的,所以长度为pos[i] - (pos[i - 1] - 1)- 1 = pos[i] - pos[i - 1]位。
代码很短,但是却很难想到
#include<bits/stdc++.h>
#define N 87
#define ll long long
using namespace std;
ll n, x, f[N + 1][2], t, pos[N + 1], fibr[N + 1], rq[N + 1];
ll sol(ll num){
t = 0; for(int i = N;i >= 1; i--) if(num >= fibr[i]) rq[++t] = i, num -= fibr[i];
for(int i = 1;i <= t; i++) pos[i] = rq[t - i + 1];
memset(f, 0, sizeof f);
f[0][1] = 1;
for(int i = 1;i <= t; i++){
f[i][1] = f[i - 1][0] + f[i - 1][1];
f[i][0] = (pos[i] - pos[i - 1] - 1) / 2 * f[i - 1][1] + (pos[i] - pos[i - 1]) / 2 * f[i - 1][0];
}
return f[t][1] + f[t][0];
}
int main()
{
// freopen("a.in", "r", stdin);
fibr[1] = 1; fibr[2] = 2; for(int i = 3;i <= N; i++) fibr[i] = fibr[i - 1] + fibr[i - 2];
scanf("%lld", &n); for(int i = 1;i <= n; i++) scanf("%lld", &x),printf("%lld\n", sol(x));
return 0;
}