题目描述{:target=”_blank”}
这里的做法,和这个做法{:target=”_blank”}的时间复杂度是相同的,但常数更小
思路
观察原式 fn=fn−1+fn−2
移项可得 fn−2=fn−fn−1
也就是 fn=fn+2−fn+1
将斐波那契的前n项都写成这种形式,得
{f1= f3−f2f2= f4−f3f3= f5−f4f4= f6−f5 ⋮fn= fn+2−fn+1
累加所有等式,
左边正好是我们要求的答案
而右边,从f1到fn+1,都互相抵消掉了,
得到
f1+f2+⋯+fn=fn+2−f2=fn+2−1
也就是说,我们就只需要求出 fn+2−1 即可
这里给出三种求的算法
算法1 矩阵快速幂
这里不再做过多赘述,关于矩阵快速幂的方法可以看下面的参考文献
时间复杂度 O(logn)
参考文献1 求斐波那契数列若干方法{:target=”_blank”}
参考文献2 如何通过递推式构造矩阵{:target=”_blank”}
C++ 代码
#include <iostream>
using namespace std;
typedef long long ll;
int n, m;
ll A[2][2] = { // 用于快速幂的矩阵
{1, 1},
{1, 0}
};
ll res[2] = {1, 0}; // 用于存答案的矩阵(转置)
// 计算列矩阵 A 乘方阵 B 的乘积,并存储在 A 中
void _multi(ll A[], ll B[][2])
{
ll ans[2] = {0};
for (int i = 0; i < 2; i ++ )
for (int j = 0; j < 2; j ++ )
ans[i] += A[j] * B[i][j] % m;
for (int i = 0; i < 2; i ++ )
A[i] = ans[i] % m;
}
// 计算方阵 A 乘方阵 B 的乘积,并存储在 A 中
void multi(ll A[][2], ll B[][2])
{
ll ans[2][2] = {0};
for (int i = 0; i < 2; i ++ )
for (int j = 0; j < 2; j ++ )
for (int k = 0; k < 2; k ++ )
ans[i][j] += A[i][k] * B[k][j] % m;
for (int i = 0; i < 2; i ++ )
for (int j = 0; j < 2; j ++ )
A[i][j] = ans[i][j] % m;
}
int main()
{
scanf("%d %d", &n, &m);
n += 2; // 求 f[n + 2] 的结果
while (n) // 矩阵快速幂板子
{
if (n & 1) _multi(res, A);
multi(A, A);
n >>= 1;
}
printf("%lld\n", res[1] - 1); // 最后 res[1] 即为 f[n + 2] 的结果
return 0;
}
算法2 推式子 + 暴力
时间复杂度 O(n) O(能过)
空间复杂度 O(n) O(超级大)
这里先给出结论,再给出证明过程
结论 fn=fkfn−k+1+fk−1fn−k (k≤n)
证明
∵
\therefore 结论对于\ k = 1\ 成立
假设结论对于\ k = i\ 成立
则有 f _ n = f _ {i} f _ {n - i + 1} + f _ {i - 1} f _ {n - i}
\Rightarrow = f _ {i}(f _ {n - i} + f _ {n - i - 1}) + f _ {i - 1} f _ {n - i}
\Rightarrow = f _ {i} f _ {n - i} + f _ {i} f _ {n - i - 1} + f _ {i - 1} f _ {n - i}
\Rightarrow = (f _ {i} + f _ {i - 1})f _ {n - i} + f _ {i} f _ {n - i - 1}
\Rightarrow = f _ {i + 1} f _ {n - i} + f _ {i} f _ {n - i - 1}
所以结论对于\ k = i + 1\ 成立,证毕
那么根据结论,当 n = 2k 时,有
f _ {2k} = f _ {k} f _ {k + 1} + f _ {k - 1} f _ {k} = f _ {k} (f _ {k + 1} + f _ {k - 1}) = f _ {k} (2 f _ {k + 1} - f _ {k})
当 n = 2k + 1 时,有
f _ {2k + 1} = f _ {k} f _ {k} + f _ {k + 1} f _ {k + 1} = f _ k ^ 2 + f _ {k + 1} ^ 2
也就是说,我们可以通过递归求出 f _ {\lfloor \frac k 2 \rfloor} 和 f _ {\lfloor \frac k 2 \rfloor + 1} 来得到 f _ k
直接这么求,时间复杂度还是 O(n),和递推相比,并没有任何优化
但是这个做法的优点,是可以快速将我们要求的 f _ n 的 n 缩小到一个很小的范围内
利用这一点,我们可以预处理出 f 的前 k 项(具体 k 的值根据题目而定)
如果递归到了 k 以内,直接返回我们预处理好的值
C++ 代码
#include <iostream>
using namespace std;
typedef long long ll;
const int k = 1e7 + 5; // 这里取 k = 1e7 + 5
int n, m;
int f[k];
// 递归求出 f[x] 的值
ll calc(int x)
{
if (x < k) return f[x]; // 如果 x 在我们预处理的 k 之内,则直接返回我们预处理出的值
ll t1 = calc(x / 2 + 1), t2 = calc(x / 2); // 递归求出 f[x / 2 + 1] 和 f[x / 2]
if (x & 1) return (t1 * t1 + t2 * t2) % m; // 按照上述等式,返回 x 是奇数时的结果
return t2 * (t1 * 2 - t2 % m + m) % m; // 返回 x 是偶数时的结果,注意这里求的是 % m 的正余数,要在括号内先加上 m,以保证结果为正数
}
int main()
{
scanf("%d %d", &n, &m);
f[1] = f[2] = 1; // 递推求出 f[0 ~ k - 1] 的值
for (int i = 3; i < k; i ++ )
f[i] = (f[i - 1] + f[i - 2]) % m;
printf("%lld\n", calc(n + 2) - 1); // 输出结果
return 0;
}
算法3\ \ 快速倍增法
这里感谢@洛零{:target=”_blank”}巨佬给出该方法
时间复杂度 O(\log n)
我们在做递归时,可以通过二元组的方式进行计算,并返回(f _ {n}, f _ {n + 1}),将时间复杂度优化至 O(\log n)
C ++ 代码
#include <cstdio>
#include <utility>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
int n, m;
// 递归求出二元组 {f[x], f[x + 1]}
pll calc(int x)
{
if (!x) return make_pair(0, 1); // 如果 x 为 0,那么返回 (0, 1)
pll u = calc(x >> 1); // 递归求出二元组 (f[x / 2], f[x / 2 + 1])
ll t1 = u.first, t2 = u.second; // 分别取出 f[x / 2] 和 f[x / 2 + 1]
ll f2 = t1 * (2 * t2 - t1 % m + m) % m; // 求出 (f[x], f[x + 1]) 中,x 为奇数的元素的值
ll f1 = (t1 * t1 + t2 * t2) % m; // 求出 (f[x], f[x + 1]) 中,x 为偶数的元素的值
if (!(x & 1)) return make_pair(f2, f1); // 如果 x 是偶数,则 f[x] = f2,直接返回 (f2, f1) 即可
return make_pair(f1, (f1 + f2) % m); // 否则说明 x 是奇数,则 f[x] = f1,需要求下 f[x + 1] 再返回
}
int main()
{
scanf("%d %d", &n, &m);
printf("%lld\n", calc(n + 2).first - 1); // 输出结果
return 0;
}
F[n+2]还可以通过算法导论中给出的F[n]=floor(phi(1.618…, 黄金分割)^i/sqrt(5)+1/2)结合快速幂食用
emm,好吧,好像不太行。。。
牛逼啊,涨知识
好厉害,都没什么值得补充的了qwq
%%%
_multi()的 ans[i] += A[j] * B[i][j] % m;是不是写错了
az小学生表示快听不懂了qwq
哈哈哈 ,参考文献2居然是自己的
感觉这个二元组的方法和那个fft的板子的思想好想
这,这是初中生?
是的,我在acwing看着他一步步成长的,他这学期结束就高一了.然而我只能打打蓝桥杯…抽风的分享还有傅里叶变换.不得不感慨自古英雄出少年..希望抽风成拿NOI金牌吧hh
好家伙,还有这么了解我的,没想到没想到
能亲眼目睹大佬的诞生,是我的荣幸…hh,抽风一定能拿金牌,跟y总做校友hh
这咋看粗他是初中生的?
以前y总讲过hh
第二种暴力递推的方法,当 k 取 2e4 的时候只用26ms,奇快无比
涨知识了,二元组tql
CHOU FENG YONG YUAN DI SHEN!!1
qwq
其实还有一种方法求斐波那契数,叫做快速倍增法,希望也能加上
已添加~