F#中的动态编程

时间:2011-11-02 18:41:02

标签: f# functional-programming

实现解决problems with overlapping subproblems的动态编程算法的最优雅方法是什么?在命令式编程中,通常会根据问题的大小创建一个索引(至少在一个维度上)的数组,然后算法将从最简单的问题开始,并使用已经计算的结果进行更复杂的迭代。

我能想到的最简单的例子是计算Nth Fibonacci数:

int Fibonacci(int N)
{
    var F = new int[N+1];
    F[0]=1;
    F[1]=1;
    for(int i=2; i<=N; i++)
    {
        F[i]=F[i-1]+F[i-2];
    }
    return F[N];
}

我知道你可以在F#中实现相同的功能,但我正在寻找一个很好的功能解决方案(显然也是O(N))。

3 个答案:

答案 0 :(得分:12)

一种对动态编程非常有用的技术称为 memoization 。有关详细信息,请参阅示例blog post by Don Symeintroduction by Matthew Podwysocki

这个想法是你编写(一个天真的)递归函数,然后添加存储以前结果的缓存。这使您可以以通常的功能样式编写函数,但可以获得使用动态编程实现的算法的性能。

例如,用于计算斐波那契数的天真(低效)函数如下所示:

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

这效率很低,因为当您拨打fibs 3时,它会拨打fibs 1三次(如果您拨打电话,则会多次拨打fibs 6)。 memoization背后的想法是我们编写一个缓存来存储fib 1fib 2的结果,依此类推,因此重复调用只会从缓存中选择预先计算的值。

执行记忆的通用函数可以这样写:

open System.Collections.Generic

let memoize(f) =    
  // Create (mutable) cache that is used for storing results of 
  // for function arguments that were already calculated.
  let cache = new Dictionary<_, _>()
  (fun x ->
      // The returned function first performs a cache lookup
      let succ, v = cache.TryGetValue(x)
      if succ then v else 
        // If value was not found, calculate & cache it
        let v = f(x) 
        cache.Add(x, v)
        v)

为了编写更有效的Fibonacci函数,我们现在可以调用memoize并为其提供执行计算的函数作为参数:

let rec fibs = memoize (fun n ->
  if n < 1 then 1 else
  (fibs (n - 1)) + (fibs (n - 2)))

请注意,这是一个递归值 - 函数体调用memoized fibs函数。

答案 1 :(得分:7)

托马斯的回答是一个很好的一般方法。在更具体的情况下,可能还有其他技术可以正常工作 - 例如,在您的斐波纳契案例中,您实际上只需要有限数量的状态(前两个数字),而不是所有先前计算的值。因此,您可以这样做:

let fibs = Seq.unfold (fun (i,j) -> Some(i,(j,i+j))) (1,1)
let fib n = Seq.nth n fibs

您也可以更直接地执行此操作(不使用Seq.unfold):

let fib =
    let rec loop i j = function
    | 0 -> i
    | n -> loop j (i+j) (n-1)
    loop 1 1

答案 2 :(得分:4)

let fibs =
    (1I,1I) 
    |> Seq.unfold (fun (n0, n1) -> Some (n0 , (n1, n0 + n1)))
    |> Seq.cache