【问题标题】:Memoization in OCaml?OCaml 中的记忆?
【发布时间】:2013-01-05 11:20:11
【问题描述】:

可以改进“原始”斐波那契递归过程

Fib[n_] := If[n < 2, n, Fib[n - 1] + Fib[n - 2]]

Fib[n_] := Fib[n] = If[n < 2, n, Fib[n - 1] + Fib[n - 2]]

在 Wolfram Mathematica 中。

第一个版本将遭受指数爆炸,而第二个版本则不会,因为 Mathematica 会在表达式中看到重复的函数调用并记忆(重用)它们。

是否可以在 OCaml 中做同样的事情?

如何改进

let rec fib n = if n<2 then n else fib (n-1) + fib (n-2);;

以同样的方式?

【问题讨论】:

    标签: recursion wolfram-mathematica ocaml fibonacci memoization


    【解决方案1】:

    rgrinberg 提供的解决方案可以泛化,以便我们可以记忆任何函数。我将使用关联列表而不是哈希表。不过没关系,您可以轻松地将我所有的示例转换为使用哈希表。

    首先,这是一个函数memo,它接受另一个函数并返回它的记忆版本。这是 nlucaroni 在其中一个 cmets 中所建议的:

    let memo f =
      let m = ref [] in
        fun x ->
          try
            List.assoc x !m
          with
          Not_found ->
            let y = f x in
              m := (x, y) :: !m ;
              y
    

    函数memo f 保存一个列表m 到目前为止计算的结果。当被要求计算 f x 时,它首先检查 m 以查看是否已经计算了 f x。如果是,则返回结果,否则实际计算f x,将结果存入m,并返回。

    如果f 是递归的,则上述memo 存在问题。一旦memo调用f计算f x,任何f的递归调用都不会被memo拦截。为了解决这个问题,我们需要做两件事:

    1. 在这种递归 f 的定义中,我们需要用对“稍后提供”的函数的调用替换递归调用(这将是 f 的记忆版本)。

    2. memo f 中,我们需要向f 提供承诺的“当您想要进行递归调用时应该调用的函数”。

    这导致以下解决方案:

    let memo_rec f =
      let m = ref [] in
      let rec g x =
        try
          List.assoc x !m
        with
        Not_found ->
          let y = f g x in
            m := (x, y) :: !m ;
            y
      in
        g
    

    为了演示它是如何工作的,让我们记住朴素的斐波那契函数。我们需要编写它以便它接受一个额外的参数,我将其称为self。这个参数是函数应该使用的,而不是递归调用自身:

    let fib self = function
        0 -> 1
      | 1 -> 1
      | n -> self (n - 1) + self (n - 2)
    

    现在要获得记忆的fib,我们计算

    let fib_memoized = memo_rec fib
    

    欢迎您试用,看看fib_memoized 50 会立即返回。 (memo f 不是这样,f 是通常的幼稚递归定义。)

    【讨论】:

    • 你的答案开头有两个错别字:“我将使用 asocitive 列表 insetad (...)”(asocitive → associative , insetad → 代替)。我不能建议少于六个字符的编辑。
    • 谢谢。你不能编辑我的答案吗? (我似乎可以编辑其他人的答案。)
    • 哦,那就引用一些莎士比亚来满足管理要求。
    • 这很棒。但这仅适用于单参数函数,对吗?不适用于多参数柯里化函数?然而,它似乎适用于更多的论点。我不明白为什么。 alist 仅使用第一个参数。啊,明白了——它只是通过传入第一个参数返回的函数,而不是带有所有参数的整个函数调用的值。
    • 什么?如果你记住f : α → β → γ,它将记住第一个参数,因为这个例子只是f : α → δ,其中δ = (β → γ)。您似乎错误地指出它正在记忆f 的结果,即β → γ 类型的函数,但事实并非如此。
    【解决方案2】:

    你几乎可以做mathematica版本的工作,但手动:

    let rec fib = 
      let cache = Hashtbl.create 10 in
      begin fun n ->
        try Hashtbl.find cache n
        with Not_found -> begin
          if n < 2 then n
          else 
            let f = fib (n-1) + fib (n-2) in
            Hashtbl.add cache n f; f
        end
      end
    

    这里我选择一个哈希表来存储已经计算的结果,而不是重新计算它们。 请注意,您仍然应该注意整数溢出,因为我们使用的是普通整数而不是大整数。

    【讨论】:

    • 可以把memoization过程抽象成高阶函数,非常方便,let memo_f f = let tbl = Hashtbl.create 100 in (fun x -&gt; if Hashtbl.mem tbl x then Hashtbl.find tbl x else begin let n = f x in Hashtbl.add tbl x n; n end)
    • @nlucaroni:这不会捕获f 进行的递归调用,所以它几乎没用。它不会降低朴素斐波那契的复杂性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-25
    • 2012-01-03
    相关资源
    最近更新 更多