0

I'm trying to implement a function defined as such:

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

The iterative way to do this would be to start at the bottom until I hit n, so if 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

Implementation attempt:

; 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)]))

Several problems:

  • The returned result is one iteration less than what it's supposed to be, because the actual calculations don't take place until the recursive call
  • Instead of using the defined algorithm to calculate f(4), I'm just putting it right in there that f(4) = 10
  • Ideally I want to start result at 0 and counter at 3 so that the calculations are applied to m1 through m4 (and so that f(4) will actually be calculated out instead of being preset), but then 0 gets used for m1 in the next iteration when it should be the result of f(4) instead (10)

tl;dr either the result calculation is delayed, or the result itself is delayed. How can I write this properly?

idlackage
  • 2,715
  • 8
  • 31
  • 52
  • 1
    Related: http://stackoverflow.com/questions/26080321/why-am-i-getting-application-not-a-procedure – Jack Oct 02 '14 at 22:47

1 Answers1

1

I think the appropriately "Scheme-ish" way to write a function that's defined recursively like that is to use memoization. If a function f is memoized, then when you call f(4) first it looks up 4 in a key-value table and if it finds it, returns the stored value. Otherwise, it simply calculates normally and then stores whatever it calculates in the table. Therefore, f will never evaluate the same computation twice. This is similar to the pattern of making an array of size n and filling in values starting from 0, building up a solution for n. That method is called dynamic programming, and memoization and dynamic programming are really different ways of looking at the same optimization strategy - avoiding computing the same thing twice. Here's a simple Racket function memo that takes a function and returns a memoized version of it:

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

Now, we can write your function f recursively without having to worry about the performance problems of ever calculating the same result twice, thus going from an exponential time algorithm down to a linear one while keeping the implementation straightforward:

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

Note that as long as the function f exists, it will keep in memory a table containing the result of every time it's ever been called.


If you want a properly tail-recursive solution, your best approach is probably to use the named let construct. If you do (let name ([id val] ...) body ...) then calling (name val ...) anywhere in body ... will jump back to the beginning of the let with the new values val ... for the bindings. An example:

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

Using this makes a tail-recursive solution for your problem much less wordy than defining a helper function and calling it:

(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)))

This version of the function keeps track of four values for f, then uses them to compute the next value and ditches the oldest value. This builds up a solution while only keeping four values in memory, and it doesn't keep a huge table stored between calls. The fn+1 helper function is for combining the four previous results of the function into the next result, it's just there for readability. This might be a function to use if you want to optimize for memory usage. Using the memoized version has two advantages however:

  1. The memoized version is much easier to understand, the recursive logic is preserved.
  2. The memoized version stores results between calls, so if you call f(10) and then f(4), the second call will only be a table lookup in constant time because calling f(10) stored all the results for calling f with n from 0 to 10.
Jack
  • 2,223
  • 13
  • 23
  • That's pretty interesting, I'll keep that in mind, thanks. If I had to make f tail-recursive though, how would I go about it? – idlackage Oct 02 '14 at 23:30