2

In the book Structure and interpretation of computer programs, there is a recursive procedure for computing exponents using successive squaring.

(define (fast-expt b n)
    (cond ((= n 0) 
        1)
    ((even? n) 
        (square (fast-expt b (/ n 2))))
   (else 
        (* b (fast-expt b (- n 1))))))

Now in exercise 1.16:

Exercise 1.16: Design a procedure that evolves an iterative exponentiation process that uses successive squaring and uses a logarithmic number of steps, as does `fast-expt`. (Hint: Using the observation that
(b(^n/2))^2 = (b(^2))^n/2

, keep, along with the exponent n and the base b, an additional state variable a, and define the state transformation in such a way that the product ab^n is unchanged from state to state. At the beginning of the process a is taken to be 1, and the answer is given by the value of a at the end of the process. In general, the technique of defining an invariant quantity that remains unchanged from state to state is a powerful way to think about the design of iterative algorithms.)

I spent a week and I absolutely can't figure how to do this iterative procedure, so I gave up and looked for solutions. All solutions I found is this:

(define (fast-expt a b n)
    (cond ((= n 0) 
        a)
    ((even? n) 
        (fast-expt a (square b) (/ n 2)))
   (else 
        (fast-expt (* a b) b (- n 1)))))

Now, I can understand

        (fast-expt a (square b) (/ n 2)))

using the hint from the book, but my brain exploded when n is odd. In the recursive procedure, I got why

        (* b (fast-expt b (- n 1))))))

works. But in the iterative procedure, it becomes totally different,

        (fast-expt (* a b) b (- n 1)))))

It's working perfectly but I absolutely don't understand how to arrive at this solution by myself. it seems extremely clever.

Can someone explain why the iterative solution is like this? And what's the general way to think of solving these types of problems?

2021 update: Last year, I completely forgot about this exercise and the solutions I've seen. I tried solving it and I finally solved it on my own using the invariant provided in the exercise as a basis for transforming the state variables. I used the now accepted answer to verify my solution. Thanks @Óscar López.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
lightning_missile
  • 2,821
  • 5
  • 30
  • 58
  • lol, I spent 30 minutes and got frustrated. You must have amazing perseverance to have spent a week on this. – dlq Jun 26 '21 at 06:35
  • @qiu it depends. On average, I spent from a week to a month solving an exercise from this book. For example, I spent almost 7 months solving exercise 1.11. I put up with it because I almost always learned something after solving them. The long time that I gave them is always worth it in the end. This is an exception because I am actually very easily distracted and frustrated if I find something to be borring. – lightning_missile Jun 27 '21 at 16:15

1 Answers1

3

Here's a slightly different implementation for making things clearer, notice that I'm using a helper procedure called loop to preserve the original procedure's arity:

(define (fast-expt b n)
  (define (loop b n acc)
    (cond ((zero? n) acc)
          ((even? n) (loop (* b b) (/ n 2) acc))
          (else (loop b (- n 1) (* b acc)))))
  (loop b n 1))

What's acc in here? it's a parameter that is used as an accumulator for the results (in the book they name this parameter a, IMHO acc is a more descriptive name). So at the beginning we set acc to an appropriate value and afterwards in each iteration we update the accumulator, preserving the invariant.

In general, this is the "trick" for understanding an iterative, tail-recursive implementation of an algorithm: we pass along an extra parameter with the result we've calculated so far, and return it in the end when we reach the base case of the recursion. By the way, the usual implementation of an iterative procedure as the one shown above is to use a named let, this is completely equivalent and a bit simpler to write:

(define (fast-expt b n)
  (let loop ((b b) (n n) (acc 1))
    (cond ((zero? n) acc)
          ((even? n) (loop (* b b) (/ n 2) acc))
          (else (loop b (- n 1) (* b acc))))))
Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • ok i understand the invariant part. But in terms of solutions, I thought the logic for iterative and recursive procedures are equivalent to each other. Thie two implementation to this problem doesn't seem to have the same logic. Am I correct? – lightning_missile Nov 22 '15 at 16:53
  • In fact they're doing _exactly the same_ logic. Both solutions end up multiplying a value by `b`, that doesn't change. In the recursive version we multiply `b` by the result of the recursive call, whereas in the iterative version the result of the recursive call is being accumulated in a parameter, but it's all the same - we just avoid having to wait for the recursion to return, by passing its result in a parameter. Think of what you do when you modify a local variable in a `for` loop in an imperative language, _it's the same here_, we're just using parameters as we would use a local variable. – Óscar López Nov 22 '15 at 17:03
  • I still don't get it if n is odd. In the recursive version, if n is odd, we do b * another function call with n-1, now n is even. I think b* is there to add another base. For example if n is 9, we do 2* fastExp 9-8, and then continue with the calls is n is even. I think 2 * is there for the ninth 2 to multiply. But I don't see b* in the iterative version. I am really confused what's happening if n is odd. – lightning_missile Nov 22 '15 at 17:14
  • It's the same idea, the same steps, the same algorithm, the only thing that changes is how we propagate the solution. In the recursive version we do `(* b (fast-expt …))`, and the solution gets propagated when we return in the stack of function calls. In the iterative version we do `(* b acc)`, because we stored in `acc` the result of the previous call to `(fast-expt …)`, and the solution gets propagated in a parameter, without incurring in the cost of having to return in the stack of function calls. – Óscar López Nov 22 '15 at 17:19
  • Consider a simpler example. A recursive procedure for adding all the integers between `0` and a given number: `(define (addFirst i) (if (zero? i) 0 (+ i (addFirst (- i 1)))))`. Now the same procedure, but iterative: `(define (addFirst i a) (if (zero? i) a (addFirst (- i 1) (+ i a))))` See how the same operation is performed? it's just that in the iterative version the solution is accumulated in the parameter `a`, which must be initialized with `0` - the exact same value the recursive procedure would return in its base case! – Óscar López Nov 22 '15 at 17:25
  • 1
    I got your example. I know how another variable is keepping track of the updated values in iterative procedures. Unfortunately I'm not able to apply it to the exponent solution if n is odd. I know why we do (* b acc) in terms of putting the values in the accumiliator, I think my confusion is from the fact that in the recursive procedure, the value of b doesn't change for every call. But for the iterative procedure, b changes a lot by squaring and squaring. Do you mind providing another example that behaves like this? – lightning_missile Nov 23 '15 at 07:41
  • @Oscar, when `n` es even I believe it would be better to square the accumulator. the helper doesn't need `b` as an argument, it should not change. – Rptx Nov 23 '15 at 11:55
  • @Rptx I don't think that'll work ;) write a proof of concept, _test_ it and prove me wrong :P – Óscar López Nov 23 '15 at 15:34
  • @Óscar López in the recursive procedure, I noticed that b doesn't change. I can understand why fastExp b n-1 is multiplied to the base (* b (fastexp)). Where does the step, the multiplying of the original base, happen in the iterative procedure? I know it's there in the a parameter, but I can't figure it out. – lightning_missile Nov 23 '15 at 15:44
  • @morbidCode In the recursive procedure `b` doesn't change, the whole value returned by the function changes because it's multiplied by `b`. Look, I can't help you anymore. Grab a piece of paper and a pen, use the substitution model and _understand_ what each process is really doing. – Óscar López Nov 23 '15 at 16:01
  • @Óscar López I tried solving it last year and I finally solved it on my own using the invariant provided in the exercise as a basis for transforming the state variables. I finally understood what you meant. For me, what it boils down to is: How would I preserve the invariant? In hindsight, I believe I don't really understand recursion that much 5 years ago. Thanks for this informative answer! – lightning_missile Jun 27 '21 at 16:37