Project Euler 第二题 Haskell 题解
最近看到了Project Euler的问题2,感觉是挺有意思的。然后打算用Haskell做一做。
问题本体如下:
1 | 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: |
简单来说的话,就是求斐波那契数列
中四百万以下
的偶数
的和。
最初的解法
很直接的想法就是直接写出来。我对于Haskell中取得一个斐波那契数列流的代码印象深刻,因为这是我所见过的最优雅的求斐波那契数列的方式了。
1 | Prelude> fib = 1:1:zipWith (+) fib (tail fib) |
然后有了这么一个流我们就可以从中取出比4 000 000
小,而且是偶数的数字进行求和即可。
首先取小于四百万的数,用takeWhile 函数能很好的完成这个目的,而且返回的已经是一个有限的队列了。
1 | Prelude> takeWhile (< 4000000) fib |
然后求其中为偶数的:
1 | Prelude> filter even it |
最后进行求和:
1 | Prelude> sum it |
这就是最终解了。如果我们把它写在文件中,最终可以只有这么两行:(用列表构造器可以代替filter)
1 | fib = 1:1:zipWith (+) fib (tail fib) |
另一种方式
虽然已经做完了这个问题,而且我觉得还有改进的空间。
最开始就是陷到了那个求出流的方式中,Haskell是默认惰性求值而且存储中间结果,因而最终我们会得到一系列的存放在内存中的斐波那契数,然而其实我们本来不需要这么多数字,我们只需要最终的求和就好了。于是考虑可以在求出数列的过程中进行筛选并且直接进行加和。
根据奇偶数的规律:
1 | 奇 + 奇 = 偶 |
我们可以发现,在斐波那契数列中,偶数的下标都3的倍数。然后再根据数列中的递推规律,可以写出来这种形式的求和:
1 | Prelude> :{ |
其中里面的z是一个偶数,然后y是偶数前面的斐波那契数。然后这里递归给的参数是求出来的下一个目标的y与z值。
这是我写的第二种解法。但是效率就很难说,因为这里其实相当于把数字算了两遍,这并不是一个非常好的办法。
这后面的方法基本都是官方给出的例子。链接在这里
更好的方法
我是完全没想到居然有这样的公式的:
$$
\begin{equation}
\begin{aligned}
A_{n} &= A_{n-1} + A_{n-2} &= A_{n-3} + 2A_{n-2} \\
&= 3A_{n-3} + 2A_{n-4} &= 3A_{n-3} + A_{n-4} + A_{n-5} + A_{n-6} \\
&= 4A_{n-3} + A_{n-6}
\end{aligned}
\end{equation}
$$
也即:$ A_{n} = 4A_{n-3} + A_{n-6} $。
如果$A_1$为0和$A_2$为4,这条公式就是满足为偶数的斐波那契数列的规律了。
然后我们可以通过斐波那契数列的通项公式估计在4000000以前的项数,可以很粗暴的得到:
1 | evenFib 0 = 0, evenFib 1 = 2, evenFib (n+2) = evenFib n + 4 * evenFib (n+1). |
这是一个很粗暴的解法,尽管有效,它的开销也是很大的。
可以由此写出来的类似于第一种的方法如下:
1 | evenFibs = 2 : 8 : zipWith (+) (map (4*) (tail evenFibs)) evenFibs |
总结
这是一道好像比较简单的小题,但是其实做法还是让我觉得学到了与之相关的不少东西。