Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No idea how to solve SICP exercise 1.11

Exercise 1.11:

A function f is defined by the rule that f(n) = n if n < 3 and f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) if n > 3. Write a procedure that computes f by means of a recursive process. Write a procedure that computes f by means of an iterative process.

Implementing it recursively is simple enough. But I couldn't figure out how to do it iteratively. I tried comparing with the Fibonacci example given, but I didn't know how to use it as an analogy. So I gave up (shame on me) and Googled for an explanation, and I found this:

(define (f n)
   (if (< n 3)
       n
       (f-iter 2 1 0 n)))

(define (f-iter a b c count)
   (if (< count 3)
       a
       (f-iter (+ a (* 2 b) (* 3 c))
               a
               b
               (- count 1))))

After reading it, I understand the code and how it works. But what I don't understand is the process needed to get from the recursive definition of the function to this. I don't get how the code could have formed in someone's head.

Could you explain the thought process needed to arrive at the solution?

like image 745
Javier Avatar asked Mar 02 '10 19:03

Javier


3 Answers

You need to capture the state in some accumulators and update the state at each iteration.

If you have experience in an imperative language, imagine writing a while loop and tracking information in variables during each iteration of the loop. What variables would you need? How would you update them? That's exactly what you have to do to make an iterative (tail-recursive) set of calls in Scheme.

In other words, it might help to start thinking of this as a while loop instead of a recursive definition. Eventually you'll be fluent enough with recursive -> iterative transformations that you won't need to extra help to get started.


For this particular example, you have to look closely at the three function calls, because it's not immediately clear how to represent them. However, here's the likely thought process: (in Python pseudo-code to emphasise the imperativeness)

Each recursive step keeps track of three things:

f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) 

So I need three pieces of state to track the current, the last and the penultimate values of f. (that is, f(n-1), f(n-2) and f(n-3).) Call them a, b, c. I have to update these pieces inside each loop:

for _ in 2..n:
    a = NEWVALUE
    b = a
    c = b
return a

So what's NEWVALUE? Well, now that we have representations of f(n-1), f(n-2) and f(n-3), it's just the recursive equation:

for _ in 2..n:
    a = a + 2 * b + 3 * c
    b = a
    c = b
return a

Now all that's left is to figure out the initial values of a, b and c. But that's easy, since we know that f(n) = n if n < 3.

if n < 3: return n
a = 2 # f(n-1) where n = 3
b = 1 # f(n-2)
c = 0 # f(n-3)
# now start off counting at 3
for _ in 3..n:
    a = a + 2 * b + 3 * c
    b = a
    c = b
return a

That's still a little different from the Scheme iterative version, but I hope you can see the thought process now.

like image 78
Nathan Shively-Sanders Avatar answered Oct 19 '22 19:10

Nathan Shively-Sanders


I think you are asking how one might discover the algorithm naturally, outside of a 'design pattern'.

It was helpful for me to look at the expansion of the f(n) at each n value:

f(0) = 0  |
f(1) = 1  | all known values
f(2) = 2  |

f(3) = f(2) + 2f(1) + 3f(0)
f(4) = f(3) + 2f(2) + 3f(1)
f(5) = f(4) + 2f(3) + 3f(2)
f(6) = f(5) + 2f(4) + 3f(3)

Looking closer at f(3), we see that we can calculate it immediately from the known values. What do we need to calculate f(4)?

We need to at least calculate f(3) + [the rest]. But as we calculate f(3), we calculate f(2) and f(1) as well, which we happen to need for calculating [the rest] of f(4).

f(3) = f(2) + 2f(1) + 3f(0)
            ↘       ↘
f(4) = f(3) + 2f(2) + 3f(1)

So, for any number n, I can start by calculating f(3), and reuse the values I use to calculate f(3) to calculate f(4)...and the pattern continues...

f(3) = f(2) + 2f(1) + 3f(0)
            ↘       ↘
f(4) = f(3) + 2f(2) + 3f(1)
            ↘       ↘
f(5) = f(4) + 2f(3) + 3f(2)

Since we will reuse them, lets give them a name a, b, c. subscripted with the step we are on, and walk through a calculation of f(5):

  Step 1:    f(3) = f(2) + 2f(1) + 3f(0) or f(3) = a1 + 2b1 +3c1

where

a1 = f(2) = 2,

b1 = f(1) = 1,

c1 = 0

since f(n) = n for n < 3.

Thus:

f(3) = a1 + 2b1 + 3c1 = 4

  Step 2:  f(4) = f(3) + 2a1 + 3b1

So:

a2 = f(3) = 4 (calculated above in step 1),

b2 = a1 = f(2) = 2,

c2 = b1 = f(1) = 1

Thus:

f(4) = 4 + 2*2 + 3*1 = 11

  Step 3:  f(5) = f(4) + 2a2 + 3b2

So:

a3 = f(4) = 11 (calculated above in step 2),

b3 = a2 = f(3) = 4,

c3 = b2 = f(2) = 2

Thus:

f(5) = 11 + 2*4 + 3*2 = 25

Throughout the above calculation we capture state in the previous calculation and pass it to the next step, particularily:

astep = result of step - 1

bstep = astep - 1

cstep = bstep -1

Once I saw this, then coming up with the iterative version was straightforward.

like image 23
mwolfetech Avatar answered Oct 19 '22 19:10

mwolfetech


Since the post you linked to describes a lot about the solution, I'll try to only give complementary information.

You're trying to define a tail-recursive function in Scheme here, given a (non-tail) recursive definition.

The base case of the recursion (f(n) = n if n < 3) is handled by both functions. I'm not really sure why the author does this; the first function could simply be:

(define (f n)
   (f-iter 2 1 0 n))

The general form would be:

(define (f-iter ... n)
   (if (base-case? n)
       base-result
       (f-iter ...)))

Note I didn't fill in parameters for f-iter yet, because you first need to understand what state needs to be passed from one iteration to another.

Now, let's look at the dependencies of the recursive form of f(n). It references f(n - 1), f(n - 2), and f(n - 3), so we need to keep around these values. And of course we need the value of n itself, so we can stop iterating over it.

So that's how you come up with the tail-recursive call: we compute f(n) to use as f(n - 1), rotate f(n - 1) to f(n - 2) and f(n - 2) to f(n - 3), and decrement count.

If this still doesn't help, please try to ask a more specific question — it's really hard to answer when you write "I don't understand" given a relatively thorough explanation already.

like image 4
Nicholas Riley Avatar answered Oct 19 '22 18:10

Nicholas Riley