实现解决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))。
答案 0 :(得分:12)
一种对动态编程非常有用的技术称为 memoization 。有关详细信息,请参阅示例blog post by Don Syme或introduction 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 1
和fib 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