偶斐波那契数列性质与欧拉计划第2题 Properties of Even Fibonacci numbers and Project Euler problems 2
Problem 2 Even Fibonacci numbers
Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, …
By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.
问题 2 偶数斐波那契数
斐波那契数列中的每个新项都是通过添加前两项来生成的。从 1 和 2 开始,前 10 项将是:
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, …
求其中小于四百万且为偶数的项之和。
目前在网上找到的大部分资料都是通过穷举斐波那契数并求和的, 这个方法针对题中 4M 以内的数据量足够迅速, 但是显然有更高效的方案.
下面将介绍简单介绍斐波那契数列的性质, 较为详细地讲解偶斐波那契数列的性质, 最后给出一个偶斐波那契数列的求和公式; 对于重要的算法, 给出rust代码示例.
如果只需要结论, 请直接看最后一节.
斐波那契数列
斐波那契数列后文记为 \(F(n)\), 定义为
欧拉计划第2题中的斐波那契数列由1开始而非从0开始, 但是从数学角度讲从0开始更方便计算.
常用的求 \(F(n)\) 算法包括递归法, 动态规划法, 公式法, 矩阵幂法, 快速倍增法. 相关算法已有很多文章介绍, 以下仅给出示例代码
1. 递归法
fn fib(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fib(n - 1) + fib(n - 2)
}
}
2. 动态规划法
通过dp数组记录已经计算的结果以减少重复计算, 同时 \(F(n)\) 只需要用 \(F(n-1)\) 和 \(F(n-2)\) 求得, 故可只保留最后两项以减少空间占用. 代码展示用dp数组的方案.
fn fib(n: u64) -> u64 {
let n = n as usize;
let mut dp = Vec::with_capacity(n + 1);
dp.push(0);
dp.push(1);
for i in 1..n {
dp.push(dp[i] + dp[i-1])
}
dp[n]
}
3. 公式法
利用通项公式
其虽然看似复杂度仅为 \(O(1)\), 但求幂过程导致实际复杂度为 \(O(\log n)\), 且存在精度问题, 不适合在n很大时使用. 其中 \((\frac{1-\sqrt{5}}{2})^n\) 恒小于1, n \(\geq\) 2开始小于0.5, 故也可使用
注意该公式前几项需要单独处理. 代码只展示原始通项公式.
fn fib(n: u64) -> u64 {
(1_f32 / 5_f32.sqrt()
* (((1_f32 + 5_f32.sqrt()) / 2_f32).powi(n as i32)
- ((1_f32 - 5_f32.sqrt()) / 2_f32).powi(n as i32)))
.round() as u64
}
4. 矩阵幂法
利用公式
或可作
可利用矩阵求幂来计算, 同时求幂过程可用快速幂加速至 \(O(\log n)\).
use std::ops::Mul;
#[derive(Copy,Debug, Clone)]
struct Mat2x2(u64, u64, u64, u64);
impl Mul for Mat2x2 {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self(
self.0 * rhs.0 + self.1 * rhs.2,
self.0 * rhs.1 + self.1 * rhs.3,
self.2 * rhs.0 + self.3 * rhs.2,
self.2 * rhs.1 + self.3 * rhs.3,
)
}
}
impl Mat2x2 {
fn pow(mut self, mut exp: u64) -> Self {
let mut ans = Self(1, 0, 0, 1);
while exp > 0 {
if exp & 1 == 1 {
ans = ans * self;
}
self = self * self;
exp >>= 1;
}
ans
}
}
fn fib(n: u64) -> u64 {
let ori = Mat2x2(0, 1, 1, 1);
ori.pow(n).1
}
5. 快速倍增法
通过矩阵幂可推导出以下公式
fn fib(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => match n % 2 {
0 => fib(n / 2) * (fib(n / 2) + 2 * fib(n / 2 - 1)),
1 => fib(n / 2 + 1).pow(2) + fib(n / 2).pow(2),
_ => panic!(),
},
}
}
通过优化能消除其中的重复计算和递归:
fn fib_pair(n: u64) -> (u64, u64) {
if n == 0 {
(0, 1)
} else {
let (mut l, mut h) = (0u64, 1u64);
let bit_length = n.ilog2();
let mut mask = 1 << bit_length;
while mask != 0 {
(l, h) = (l * (2 * h - l), l * l + h * h);
if n & mask != 0 {
(l, h) = (h, l + h);
}
mask >>= 1;
}
(l, h)
}
}
fn fib(n: u64) -> u64 {
fib_pair(n).0
}
通过复杂度分析可以发现其与矩阵快速幂一样达到\(O(\log n)\), 但快速倍增法优化后的常数要小于矩阵快速幂. 如果对斐波那契数列算法感兴趣, 可以移步到这篇文章, 这里会进一步介绍优化的方案,并提出了一个新的 \(O(\log n)\) 算法: 扩域法.
偶斐波那契数列
由斐波那契数列中的偶数构成的数列, 下文用\(G(n)\)表示.
显然有 \(G(n)=F(3n)\), 可以用求和的奇偶性来证明.
同时 \(G(n)\) 满足以下递推公式
该递推公式可由 \(F(n)=F(n-1)+F(n-2)\) 和 \(F(n)=F(n+1)-F(n-1)\) 推导出来:
既然\(G(n)\)与\(F(n)\)有相似的递推公式, 两者就有相似的性质, 以上所有算法都适用于\(G(n)\).
1. 递归法
fn even_fib(n: u64) -> u64 {
match n {
0 => 0,
1 => 2,
_ => even_fib(n - 1) * 4 + even_fib(n - 2),
}
}
2. 动态规划法
fn even_fib(n: u64) -> u64 {
let n = n as usize;
let (mut f1, mut f2) = (0, 2);
for _ in 0..n {
let f3 = f1 + f2 * 4;
f1 = f2;
f2 = f3;
}
f1
}
3. 公式法
所有形如
的递推公式都存在通项公式, 通过以下方法求解
- 建立并求解特征方程
通过递推式得到特征方程为:$$r^2 - ar - b = 0$$
假设求得解为 \(r_1\) 与\(r_2\). - 通过根确定通项公式
若存在两实根 \(r_1\neq r_2\) 则通项公式为: $$f(n)=A r_1^n + Br_2^n$$
若 \(r_1 = r_2\) 则通项公式为: $$f(n)=(A+Bn)r^n$$
若存在一对共轭复根 \(r_{1,2} = a\pm bi\) 则通项公式仍为: $$f(n)=A r_1^n + Br_2^n$$
将复数改写为指数形式并通过棣莫弗公式简化, 可得到三角形式: $$f(n) = r^n((A+B)\cos n\theta + i(A-B)\sin n\theta)$$
其中
- 确定系数
注意到2中所有通项公式均存在两个待定系数A和B, 故需要带入两个边界条件求解得到
通过上述三个步骤可得 \(G(n)\) 通项公式为
同样存在精度问题, 其中 \((2 - \sqrt{5})^n\) 恒小于0.5, 也可省略并四舍五入. 代码展示原始通项公式
fn even_fib(n: u64) -> u64 {
(1_f32 / 5_f32.sqrt()
* (
(2_f32 + 5_f32.sqrt()).powi(n as i32)
- (2_f32 - 5_f32.sqrt()).powi(n as i32))
).round() as u64
}
4. 矩阵幂
类似斐波那契数列, 假设有
可以解得
也可作
故也可用矩阵幂或矩阵快速幂求解
之所以这个矩阵求幂能得到数列, 因为该矩阵同样能够满足递推公式的特征方程:
斐波那契数列的矩阵同理:
use std::ops::Mul;
#[derive(Copy, Debug, Clone)]
struct Mat2x2(u64, u64, u64, u64);
impl Mul for Mat2x2 {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self(
self.0 * rhs.0 + self.1 * rhs.2,
self.0 * rhs.1 + self.1 * rhs.3,
self.2 * rhs.0 + self.3 * rhs.2,
self.2 * rhs.1 + self.3 * rhs.3,
)
}
}
impl Mul<u64> for Mat2x2 {
type Output = Self;
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * rhs, self.1 * rhs, self.2 * rhs, self.3 * rhs)
}
}
impl Mat2x2 {
fn pow(mut self, mut exp: u64) -> Self {
let mut ans = Self(1, 0, 0, 1);
while exp > 0 {
if exp & 1 == 1 {
ans = ans * self;
}
self = self * self;
exp >>= 1;
}
ans
}
}
fn even_fib(n: u64) -> u64 {
let ori = Mat2x2(0, 1, 1, 4);
(ori.pow(n) * 2).1
}
5. 快速倍增法
利用矩阵幂公式
可得
按矩阵乘法可得两个不同形式的公式:
利用以上公式得到以下算法:
fn even_fib_pair(n: u64) -> (u64, u64) {
if n == 0 {
(0, 2)
} else {
let (mut l, mut h) = (0u64, 2u64);
let bit_length = n.ilog2();
let mut mask = 1 << bit_length;
while mask != 0 {
(l, h) = (2 * l * (h/2 - l), l * l/2 + h * h/2);
if n & mask != 0 {
(l, h) = (h, l + 4 * h);
}
mask >>= 1;
}
(l, h)
}
}
fn even_fib(n: u64) -> u64 {
even_fib_pair(n).0
}
求和公式
斐波那契数列和偶斐波那契数列均存在简单的求和公式, 故欧拉计划第2题实际上可以用快速幂或快速倍增法以 \(O(\log n)\) 求特定项, 随后用求和公式以 \(O(1)\) 得到求和结果. 下面给出偶斐波那契数列求和公式证明方法, 斐波那契数列求和公式更简单, 请读者自行推导.
令
有
解得
利用求和公式和前文的求数列方法, 可以最快以 \(O(\log n)\) 求0至n项和, 以下为快速幂和快速倍增法的示例代码, 注意其中 even_fib 函数改为even_fib_pair 函数, 返回值从 \(G(n)\) 变成了 \((G(n+1), G(n))\).
- 快速幂
use std::ops::Mul;
fn s(n: u64) -> u64 {
let (gn_1, gn) = even_fib_pair(n);
(gn_1 + gn - 2) / 4
}
#[derive(Copy, Debug, Clone)]
struct Mat2x2(u64, u64, u64, u64);
impl Mul for Mat2x2 {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self(
self.0 * rhs.0 + self.1 * rhs.2,
self.0 * rhs.1 + self.1 * rhs.3,
self.2 * rhs.0 + self.3 * rhs.2,
self.2 * rhs.1 + self.3 * rhs.3,
)
}
}
impl Mul<u64> for Mat2x2 {
type Output = Self;
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * rhs, self.1 * rhs, self.2 * rhs, self.3 * rhs)
}
}
impl Mat2x2 {
fn pow(mut self, mut exp: u64) -> Self {
let mut ans = Self(1, 0, 0, 1);
while exp > 0 {
if exp & 1 == 1 {
ans = ans * self;
}
self = self * self;
exp >>= 1;
}
ans
}
}
fn even_fib_pair(n: u64) -> (u64, u64) {
let ori = Mat2x2(0, 1, 1, 4);
let mat = ori.pow(n) * 2;
(mat.3, mat.1)
}
- 快速倍增法
fn s(n: u64) -> u64 {
let (gn_1, gn) = even_fib_pair(n);
(gn_1 + gn - 2) / 4
}
fn even_fib_pair(n: u64) -> (u64, u64) {
if n == 0 {
(2, 0)
} else {
let (mut h, mut l) = (2u64, 0u64);
let bit_length = n.ilog2();
let mut mask = 1 << bit_length;
while mask != 0 {
(l, h) = (2 * l * (h / 2 - l), l * l / 2 + h * h / 2);
if n & mask != 0 {
(l, h) = (h, l + 4 * h);
}
mask >>= 1;
}
(h, l)
}
}
虽然这两种方法都可以由 \(O(\log n)\) 求和, 但是无法提前预知哪一项开始大于 4M, 如果仅针对欧拉计划第2题, 采用偶斐波那契数列的动态规划法, 每项判断其是否小于 4M 也很高效, 从数学角度看最好的方法则是用公式法计算
得到
即可得
本文来自博客园,作者:Meth_nylon,转载请注明原文链接:https://www.cnblogs.com/Meth-nylon/p/18662930

浙公网安备 33010602011771号