背景

众所周知,Haskell语言是一门函数式编程语言。函数式编程语言的一大特点就是数值和对象都是不可变的,而这与经常需要对状态目前的值进行修改的动态规划算法似乎有些“格格不入”,本文对几乎可以说是动态规划的最简单特例:斐波那契数列的求解提出几种算法(不包括矩阵快速幂优化、Monad和通项公式计算),探讨一下函数式编程如何结合动态规划。

自底向上写法

算法1:

f' 1 _ b = b
f' n a b = f' (n - 1) b (a + b)
f n = f' n 0 1

尾递归,所以本质上和其他语言循环递推计算是一样的,但是如果编译器没看出来而真的用递归去算可能会爆栈。

算法2:

f' (a, b) _ = (b, a + b)
f n = snd (foldl f' (0, 1) (take (n - 1) (repeat 0)))

和上面算法一样,但是用fold来写的话可以保证编译器优化掉递归而不会爆栈。同时,在我看来,fold的过程体现了状态的变化,初状态通过一步步的计算得到末状态,正和动态规划的思想相契合,所以我认为这是动态规划的递推形式在函数式编程语言里最好的写法。

算法3:

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
f n = fibs !! n

摘自https://wiki.haskell.org/The_Fibonacci_sequence,这个应该是最接近斐波那契数列的数学递推式的写法了,构造一个无限序列(fibs),然后描述序列是由0、1、fibs[0:n]和fibs[1:n+1]相加构成,最后当执行函数f n时,才开始对fibs[n]进行计算。这个算法充满了数学的味道,但如果从计算机的角度则非常难分析,当然也有可能是我太弱,至少我是写不出这样的算法。原网页还有很多类似的算法,但是我智商不给用很多看不懂。

自顶向下写法

算法1:

f 0 = 0
f 1 = 1
f x = (f (x - 1)) + (f (x - 2))

秉承了自顶向下写法一贯的好懂,但是作为教科书级的待优化代码,也是最慢的。

算法2:

a = map f [0..]
f 0 = 0
f 1 = 1
f x = a !! (x - 1) + a !! (x - 2)

这个可以说是比较标准的函数式语言里的记忆化搜索了,先声明一个无穷序列a存储的值是对0到无穷施加函数f的结果,但由于惰性求值的机制,一开始a的值是没有经过计算的,只有在递归的过程中遇到了要求a[x-1]和a[x-2]时才会去求,而求a[x-1]和a[x-2]就是求f(x-1)和f(x-2),实现了搜索,同时当a[x]求出来之后,如果之后还需要a[x]就直接取值就行了,因此也实现了记忆化。美中不足的是,haskell里的list是用链表实现的,因此取索引需要O(n)的复杂度,比较慢。

算法3:

import Data.Sequence
f n = let
a = fromFunction (n + 1) f'
f' 0 = 0
f' 1 = 1
f' x = a `index` (x - 1) + a `index` (x - 2)
in f' n

和上面那个算法差不多,但是使用Sequence代替list,Sequence用的是BST,索引复杂度是O(logn),虽然还是有点浪费,不过也差不多了。haskell里有一个array,虽然支持O(1)索引,但是没有什么map、fromFunction之流可以用。有一个库Vector据说可以符合这样的要求(惰性求值+map或fromFunction+O(1)索引),不过因为不是标准库所以我没尝试。注意Sequence不支持无限长度,同时fromFunction传给f'的是length-1,故传的值是n+1。

总结

目前看来,haskell计算斐波那契数列的方法中,自底向上法效率最高且容易懂的应该是使用fold的方法,自顶向下法效率最高的应该是利用一个O(1)索引且支持惰性求值的数据结构作为记忆表进行记忆化搜索的方法。显然,使用自底向上更优一些,这在其他范式的语言也是一样,没有递归负担,容易优化(滚动数组省空间、特定问题使用单调队列、斜率优化等),而且部分函数式编程语言不支持惰性求值,则直接关上了记忆化搜索的大门。总的说来,斐波那契数列还是一个极为简单的例子,函数式语言实现动态规划,仍然是值得深入研究的一个问题。

关于Haskell计算斐波那契数列的思考的更多相关文章

  1. 使用并行的方法计算斐波那契数列 (Fibonacci)

    更新:我的同事Terry告诉我有一种矩阵运算的方式计算斐波那契数列,更适于并行.他还提供了利用TBB的parallel_reduce模板计算斐波那契数列的代码(在TBB示例代码的基础上修改得来,比原始 ...

  2. Android NDK入门实例 计算斐波那契数列一生成jni头文件

    最近要用到Android NDK,调用本地代码.就学了下Android NDK,顺便与大家分享.下面以一个具体的实例计算斐波那契数列,说明如何利用Android NDK,调用本地代码.以及比较本地代码 ...

  3. 以计算斐波那契数列为例说说动态规划算法(Dynamic Programming Algorithm Overlapping subproblems Optimal substructure Memoization Tabulation)

    动态规划(Dynamic Programming)是求解决策过程(decision process)最优化的数学方法.它的名字和动态没有关系,是Richard Bellman为了唬人而取的. 动态规划 ...

  4. 用递归方法计算斐波那契数列(Recursion Fibonacci Sequence Python)

    先科普一下什么叫斐波那契数列,以下内容摘自百度百科: 斐波那契数列(Fibonacci sequence),又称黄金分割数列.因意大利数学家列昂纳多·斐波那契(Leonardoda Fibonacci ...

  5. shell脚本计算斐波那契数列

    计算斐波那契数列 [1,1,2,3,5,8,,,,,] #!/bin/bash n=$ num=( ) i= while [[ $i -lt $n ]] do let num[$i]=num[$i-] ...

  6. java 递归及其经典应用--求阶乘、打印文件信息、计算斐波那契数列

    什么是递归 我先看下百度百科的解释: 一种计算过程,如果其中每一步都要用到前一步或前几步的结果,称为递归的.用递归过程定义的函数,称为递归函数,例如连加.连乘及阶乘等.凡是递归的函数,都是可计算的,即 ...

  7. X86汇编——计算斐波那契数列程序(详细注释和流程图说明)

    X86汇编实现斐波那契数列 程序说明: 输入斐波那契数列的项数, 然后依次输出斐波那契数列, 输入的项数小于256且为数字, 计算的项数不能超过2^16次方, 输入失败是 不会回显数字 因为存结果是A ...

  8. python计算斐波那契数列

    斐波那契数列就是黄金分割数列 第一项加第二项等于第三项,以此类推 第二项加第三项等于第四项 代码如下 这一段代码实现fib(n)函数返回第n项,PrintFN(m,n,i)函数实现输出第i项斐波那契数 ...

  9. Android NDK入门实例 计算斐波那契数列二生成.so库文件

    上一篇文章输生成了jni头文件,里面包含了本地C代码的信息,提供我们引用的C头文件.下面实现本地代码,再用ndk-build编译生成.so库文件.由于编译时要用到make和gcc,这里很多人是通过安装 ...

随机推荐

  1. 使用三台云服务器搭建真正的Redis集群

    三台云服务器搭建redis集群# 今天花了一天的时间弄集群redis:遇到了很多坑,从头开始吧 环境讲解: 两台配置:1核2G,另一台:1核1G: 操作系统:Centos 7.6 Redis:3.2. ...

  2. MAC App破解之路十 Particle Design

    这个软件破解非常简单: 修改: [PaddleStatic Yz6nrtNwF4].直接返回1 效果:

  3. 三星note8港版如何显示电量百分比呢?

    设置-通知-状态栏,显示电量百分比打钩即可.

  4. 用python玩推理游戏还能掌握基础知识点,有趣又充实,你不试试吗?

    可能更多的人依然还在苦苦的学python各种知识点,但其实同样很多人,玩着游戏就把python学会了.     用python玩推理游戏,是这份python教程中的12个游戏的其中之一. 有关这份Py ...

  5. Java学习笔记6(集合类)

    集合类 集合按照其存储结构可以分为两大类,即单列集合Collection和双列集合Map. Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,有List和Set两个重要子接口 ...

  6. 【案例演示】JVM之强引用、软引用、弱引用、虚引用

    1.背景 想要理解对象什么时候回收,就要理解到对象引用这个概念,于是有了下文 2.java中引用对象结构图 3.引用详解 3.1.什么是强引用 a.当内存不足,JVM开始垃圾回收,对于强引用的对象,就 ...

  7. Ubuntu下安装PIL

    Ubuntu下安装PIL 1)sudo apt-get install libjpeg-dev 2)sudo apt-get install libfreetype6-dev 3)sudo easy_ ...

  8. const变量的修改

    int main(){ const char a[]="hello world"; char *aa=(char *)a; printf("\nthe a address ...

  9. window 开机重启运行bat文件(运行java的jar包)

    1:直接打开电脑的目录:(记得勾选查看隐藏项目) C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp,在里面新建start.bat ...

  10. 手写React的Fiber架构,深入理解其原理

    熟悉React的朋友都知道,React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重 ...