方案中的迭代树计算

时间:2014-10-02 22:16:14

标签: scheme iteration racket

我试图实现这样定义的函数:

f(n) = n if n < 4
f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) + 4f(n - 4) if n >= 4

执行此操作的迭代方法是从底部开始直到我点击n,所以如果n = 6:

f(4) = (3) + 2(2) + 3(1) + 4(0)     | 10
f(5) = f(4) + 2(3) + 3(2) + 4(1)    | 10  + 16 = 26
f(6) = f(5) + 2f(4) + 3(3) + 4(2)   | 26 + 2(10) + 17 = 63

实施尝试:

; m1...m4 | The results of the previous calculations (eg. f(n-1), f(n-2), etc.)
; result  | The result thus far
; counter | The current iteration of the loop--starts at 4 and ends at n
(define (fourf-iter n)
  (cond [(< n 4) n]
        [else
         (define (helper m1 m2 m3 m4 result counter)
           (cond [(= counter n) result]
                 [(helper result m1 m2 m3 (+ result m1 (* 2 m2) (* 3 m3) (* 4 m4)) (+ counter 1))]))
         (helper 3 2 1 0 10 4)]))

几个问题:

  • 返回的结果是一次迭代,小于它应该是的,因为实际的计算不会发生,直到递归调用
  • 我没有使用定义的算法来计算f(4),而是将其正确地放在那里f(4) = 10
  • 理想情况下,我想在0开始result,在3开始counter,以便将计算应用于m1m4(以及f(4)实际上是计算出来而不是预设的),但是0在下一次迭代中被用作m1,而它应该是f(4)的结果(10

tl; dr结果计算延迟,或结果本身延迟。我怎样才能正确写出来?

1 个答案:

答案 0 :(得分:1)

我认为编写一个递归定义的函数的相应“Scheme-ish”方法就是使用 memoization 。如果函数f memoized ,那么当您首先调用f(4)时,它会在键值表中查找4,如果找到它,则返回储值。否则,它只是正常计算,然后存储它在表中计算的任何内容。因此,f 永远不会评估相同的计算两次。这类似于制作大小为n的数组并填充从0开始的值的模式,为n构建解决方案。该方法称为动态编程,而memoization和动态编程实际上是查看相同优化策略的不同方式 - 避免两次计算相同的事情。这是一个简单的Racket函数memo,它接受​​一个函数并返回它的memoized版本:

(define (memo f)
  (let ([table (make-hash)])
    (lambda args
      (hash-ref! table
                 args
                 (thunk (apply f args))))))

现在,我们可以递归地编写函数f,而不必担心两次计算相同结果的性能问题,从而从指数时间算法下降到线性算法,同时保持实现简单:

(define f
  (memo
    (lambda (n)
      (if (< n 4)
          n
          (+ (f (- n 1))
             (* 2 (f (- n 2)))
             (* 3 (f (- n 3)))
             (* 4 (f (- n 4))))))))

请注意,只要函数f存在,它就会在内存中保存一个表,其中包含每次调用它时的结果。


如果你想要一个正确的尾递归解决方案,你最好的方法可能是使用命名的let 结构。如果您执行(let name ([id val] ...) body ...),则在(name val ...)中的任何位置调用body ...将跳回let的开头,并为绑定添加新值val ...。一个例子:

(define (display-n string n)
  (let loop ([i 0])
    (when (< i n)
        (display string)
        (loop (add1 i)))))

使用它可以为你的问题提供一个尾递归解决方案,而不是定义一个辅助函数并调用它:

(define (f n)
  (if (< n 4)
      n
      (let loop ([a 3] [b 2] [c 1] [d 0] [i 4])
        (if (<= i n)
            (loop (fn+1 a b c d) a b c (add1 i))
            a))))

(define (fn+1 a b c d)
  (+ a (* 2 b) (* 3 c) (* 4 d)))

此版本的函数会跟踪f的四个值,然后使用它们计算下一个值并删除最旧的值。这构建了一个解决方案,同时只在内存中保留了四个值,并且它不会在调用之间保留一个巨大的表。 fn+1辅助函数用于将函数的前四个结果组合到下一个结果中,它只是为了便于阅读。如果要优化内存使用,可能需要使用此功能。使用memoized版本有两个优点:

  1. 记忆版本更容易理解,保留了递归逻辑。
  2. memoized版本在调用之间存储结果,因此如果您调用f(10)然后调用f(4),则第二次调用将只是在常量时间内查找表,因为调用{ {1}}存储了调用f(10) f从0到10的所有结果。