【问题标题】:Iterative tree calculation in scheme方案中的迭代树计算
【发布时间】:2014-10-02 22:16:13
【问题描述】:

我正在尝试实现一个这样定义的函数:

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 结果计算被延迟,或者结果本身被延迟。我怎样才能正确地写这个?

【问题讨论】:

标签: scheme iteration racket


【解决方案1】:

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

(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 存在,它就会在内存中保存一个包含每次调用结果的表。


如果您想要一个适当的尾递归解决方案,您最好的方法可能是使用 named let 构造。如果您执行(let name ([id val] ...) body ...),那么在body ... 中的任何位置调用(name val ...) 将跳转到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. memoized 版本更容易理解,保留了递归逻辑。
  2. memoized 版本存储结果在调用之间,所以如果你调用f(10) 然后f(4),第二次调用将只是一个恒定时间的表查找,因为调用f(10) 存储了所有使用 n 从 0 到 10 调用 f 的结果。

【讨论】:

  • 这很有趣,我会记住的,谢谢。如果我必须让 f 尾递归,我会怎么做?
猜你喜欢
  • 2021-02-16
  • 1970-01-01
  • 2019-03-14
  • 2022-01-21
  • 1970-01-01
  • 1970-01-01
  • 2012-09-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多