-25

I've started reading the book Systematic Program Design: From Clarity to Efficiency few days ago. Chapter 4 talks about a systematic method to convert any recursive algorithm into its counterpart iterative. It seems this is a really powerful general method but I'm struggling quite a lot to understand how it works.

After reading a few articles talking about recursion removal using custom stacks, it feels like this proposed method would produce a much more readable, optimized and compact output.


Recursive algorithms in Python where I want to apply the method

#NS: lcs and knap are using implicit variables (i.e.: defined globally), so they won't
#work directly

# n>=0
def fac(n):
    if n==0:
        return 1
    else:
        return n*fac(n-1)

# n>=0
def fib(n):
    if n==0:
        return 0
    elif n==1:
        return 1
    else:
        return fib(n-1)+fib(n-2)

# k>=0, k<=n
def bin(n,k):
    if k==0 or k==n:
        return 1
    else:
        return bin(n-1,k-1)+bin(n-1,k)

# i>=0, j>=0
def lcs(i,j):
    if i==0 or j==0:
        return 0
    elif x[i]==y[j]:
        return lcs(i-1,j-1)+1
    else:
        return max(lcs(i,j-1),lcs(i-1,j))

# i>=0, u>=0,  for all i in 0..n-1 w[i]>0
def knap(i,u):
    if i==0 or u==0:
        return 0
    elif w[i]>u:
        return knap(i-1,u)
    else:
        return max(v[i]+knap(i-1,u-w[i]), knap(i-1,u))

# i>=0, n>=0
def ack(i,n):
    if i==0:
        return n+1
    elif n==0:
        return ack(i-1,1)
    else:
        return ack(i-1,ack(i,n-1))

Step Iterate: Determine minimum increments, transform recursion into iteration

The Section 4.2.1 the book talks about determining the appropriate increment:

1) All possible recursive calls
    fact(n)   => {n-1}
    fib(n)    => {fib(n-1), fib(n-2)}
    bin(n,k)  => {bin(n-1,k-1),bin(n-1,k)}
    lcs(i,j)  => {lcs(i-1,j-1),lcs(i,j-1),lcs(i-1,j)}
    knap(i,u) => {knap(i-1,u),knap(i-1,u-w[i])}
    ack(i,n)  => {ack(i-1,1),ack(i-1,ack(i,n-1)), ack(i,n-1)}

2) Decrement operation
    fact(n)   => n-1
    fib(n)    => n-1
    bin(n,k)  => [n-1,k]
    lcs(i,j)  => [i-1,j]
    knap(i,u) => [i-1,u]
    ack(i,n)  => [i,n-1]

3) Minimum increment operation
    fact(n)   => next(n) = n+1
    fib(n)    => next(n) = n+1
    bin(n,k)  => next(n,k) = [n+1,k]
    lcs(i,j)  => next(i,j) = [i+1,j]
    knap(i,u) => next(i,u) = [i+1,u]
    ack(i,n)  => next(i,n) = [i,n+1]

Section 4.2.2 talks about forming the optimized program:

Recursive
---------
def fExtOpt(x):
    if base_cond(x) then fExt0(x )       -- Base case
    else let rExt := fExtOpt(prev(x)) in -- Recursion
        f Ext’(prev(x),rExt)              -- Incremental computation

Iterative
---------
def fExtOpt(x):
    if base_cond(x): return fExt0(x)                    -- Base case
    x1 := init_arg; rExt := fExt0(x1)                   -- Initialization
    while x1 != x:                                      -- Iteration
        x1 := next(x1); rExt := fExt’(prev(x1),rExt)    -- Incremental comp
    return rExt

How do I create {fibExtOpt,binExtOpt,lcsExtOpt,knapExtOpt,ackExtOpt} in Python?

Additional material about this topic can be found in one of the papers of the main author of the method, Y. Annie Liu, Professor.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
BPL
  • 9,632
  • 9
  • 59
  • 117
  • Here's another more general question about this subject : http://stackoverflow.com/questions/159590/way-to-go-from-recursion-to-iteration#159777 . If you need some help derecursivating some code, then post it so we can help you more efficiently. – HolyDanna Jul 25 '16 at 12:29
  • 2
    What you asking then? Post some code so we can give you advises. Generally in Python recursions are slower than normal loops because of the cPython implementation of frame objects. – Or Duan Jul 25 '16 at 12:29
  • @HolyDanna I'll edit my question to add a simple set of naive recursive algorithms I'm using in order to understand that **book's method**, not other like the one mentioned in your link using custom stacks. – BPL Jul 25 '16 at 12:44
  • @OrDuan As I've mentioned I'm trying to understand the method mentioned in that book to convert recursive into iterative. I'll add some examples showing where I've got stuck – BPL Jul 25 '16 at 12:47
  • In general, we want you to post a self-contained question. Giving us the link to buy the book on Amazon really doesn't cover the required background. I expect that you're not allowed to post the applicable material from the book; as a result, this question won't be appropriate for StackExchange. – Prune Jul 25 '16 at 21:24
  • @Prune I've referenced the book so people who already read and understood that part of the book could help explaining how to apply the method onto a simple set of python examples (the book uses a different agnostic language). I don't understand why you're saying the question is not appropiate for StackExchange. IMHO, this question is like trying to solve some doubts after reading some mathematical method and then making some references to such theorem (ie: Cooley–Tukey FFT algorithm linking one of the many papers/books talking about) wouldn't be in that case appropriate neither? – BPL Jul 26 '16 at 06:23
  • That particular point turns on whether the referenced information is publicly available or small enough to include. – Prune Jul 26 '16 at 17:34
  • @Prune I've asked the author if it was alright to post this type of question here and she didn't mind to post it. Now, I have been posting stuff in SO for few weeks already and I still don't understand why the question i've formulated in this post, which is very interesting and relevant has got negative votes and no answers at all and other trivial questions out there become really popular among people very fast. – BPL Aug 10 '16 at 20:52
  • 8
    In my opinion, the lack of approval and lack of response are because this is not yet a self-contained question. For comparison, we can easily find references on line for Ackermann's function, the binomial function, and the others referenced. What we cannot find easily is section 4.3. Right now, your audience is limited to people who have read and understood chapter 4, and are willing to give you a tutorial on it. Tutorials are *generally* beyond the scope of SO. – Prune Aug 10 '16 at 23:48
  • I agree that this is an interesting question. I wish I could help with an answer, but I don't have the book, and my efforts to find the process elsewhere quickly met with dead ends. – Prune Aug 10 '16 at 23:49
  • I don't have the book but [Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming#Dynamic_programming_in_computer_programming) is about storing the results of intermediate results in case of reuse (like `fib(i)` and `fib(i-1)` will both need `fib(i-2)`) – λuser Aug 13 '16 at 12:54
  • @Prune Dr.Annie Liu (the main author of this method) mentioned me also this [paper](http://www3.cs.stonybrook.edu/~liu/papers/IncEff-HOSC00.pdf) would also help me to understand her method. In any case, If you got some suggestions to improve the quality of the question in order to get some answers, please do so. It's a shame that such interesting topic won't reach to any SO expert on this particular field. – BPL Aug 15 '16 at 15:26
  • Thanks for the reference to the paper. I'm still not picking up enough background to be of help. – Prune Aug 15 '16 at 22:37
  • @Prune That's alright, Dr.Liu already explained to me this is definitely a non-trivial subject and lots of experts in the field with years of experience struggle quite a lot. Hopefully I'll be editing the main post to make it more visible to the SO's experts. – BPL Aug 15 '16 at 22:51
  • 2
    Maybe this is better suited to the http://programmers.stackexchange.com/ – gibertoni Aug 17 '16 at 19:59
  • @KuramaYoko when referring other sites, it is often helpful to point that [cross-posting is frowned upon](http://meta.stackexchange.com/tags/cross-posting/info) – gnat Aug 18 '16 at 06:30
  • @gnat I am sorry for not poiting that out. Just to know, is it possible to a moderator to move a question from one site to another? But anyways, he can ask to delete his question here and start a new one there, right? – gibertoni Aug 19 '16 at 14:28
  • @BPL, only a college professor would write a paper on optimization, and choose LISP as the language to use in examples. There are a couple of flaws in that paper, too. For one, her first method of insertion sort--which she admits is complexity O(n2)--is NOT how insertion sort is normally done. She derives the normal method of insertion sort, which she calls the optimized version. I do not have the book but the method of the paper's CACHET system is to pull an initial recursion into its individual g(x), g1(x) steps and then try to find an h(x) that jumps to the result directly. – Daniel Wisehart Aug 23 '16 at 14:59
  • @Peter Mortensen I haven't received any answer but it's really appreciated you've helped to edit the question, is it possible to give you the bounties to you even if you didn't answer? I wouldn't want them to be wasted – BPL Aug 24 '16 at 15:40

1 Answers1

4

So, to restate the question. We have a function f, in our case fac.

def fac(n):
    if n==0:
        return 1
    else:
        return n*fac(n-1)

It is implemented recursively. We want to implement a function facOpt that does the same thing but iteratively. fac is written almost in the form we need. Let us rewrite it just a bit:

def fac_(n, r):
    return (n+1)*r

def fac(n):
    if n==0:
        return 1
    else:
        r = fac(n-1)
        return fac_(n-1, r)

This is exactly the recursive definition from section 4.2. Now we need to rewrite it iteratively:

def facOpt(n):
    if n==0:
        return 1
    x = 1
    r = 1
    while x != n:
        x = x + 1
        r = fac_(x-1, r)
    return r

This is exactly the iterative definition from section 4.2. Note that facOpt does not call itself anywhere. Now, this is neither the most clear nor the most pythonic way of writing down this algorithm -- this is just a way to transform one algorithm to another. We can implement the same algorithm differently, e.g. like that:

def facOpt(n):
    r = 1
    for x in range(1, n+1):
        r *= x 
    return r

Things get more interesting when we consider more complicated functions. Let us write fibObt where fib is :

def fib(n):
    if n==0:
        return 0
    elif n==1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

fib calls itself two times, but the recursive pattern from the book allows only a single call. That is why we need to extend the function to returning not one, but two values. Fully reformated, fib looks like this:

def fibExt_(n, rExt):
    return rExt[0] + rExt[1], rExt[0]

def fibExt(n):
    if n == 0:
        return 0, 0
    elif n == 1:
        return 1, 0
    else:
        rExt = fibExt(n-1)
        return fibExt_(n-1, rExt)

def fib(n):
    return fibExt(n)[0]

You may notice that the first argument to fibExt_ is never used. I just added it to follow the proposed structure exactly. Now, it is again easy to turn fib into an iterative version:

def fibExtOpt(n):
    if n == 0:
        return 0, 0
    if n == 1:
        return 1, 0
    x = 2
    rExt = 1, 1
    while x != n:
        x = x + 1
        rExt = fibExt_(x-1, rExt)
    return rExt

def fibOpt(n):
    return fibExtOpt(n)[0]

Again, the new version does not call itself. And again one can streamline it to this, for example:

def fibOpt(n):
    if n < 2:
        return n
    a, b = 1, 1
    for i in range(n-2):
        a, b = b, a+b
    return b

The next function to translate to iterative version is bin:

def bin(n,k):
    if k == 0 or k == n:
        return 1
    else:
        return bin(n-1,k-1) + bin(n-1,k)

Now neither x nor r can be just numbers. The index (x) has two components, and the cache (r) has to be even larger. One (not quite so optimal) way would be to return the whole previous row of the Pascal triangle:

def binExt_(r):
    return [a + b for a,b in zip([0] + r, r + [0])]

def binExt(n):
    if n == 0:
        return [1]
    else:
        r = binExt(n-1)
        return binExt_(r)

def bin(n, k):
    return binExt(n)[k]

I have't followed the pattern so strictly here and removed several useless variables. It is still possible to translate to an iterative version directly:

def binExtOpt(n):
    if n == 0:
        return [1]
    x = 1
    r = [1, 1]
    while x != n:
        r = binExt_(r)
        x += 1
    return r

def binOpt(n, k):
    return binExtOpt(n)[k]

For completeness, here is an optimized solution that caches only part of the row:

def binExt_(n, k_from, k_to, r):
    if k_from == 0 and k_to == n:
        return [a + b for a, b in zip([0] + r, r + [0])]
    elif k_from == 0:
        return [a + b for a, b in zip([0] + r[:-1], r)]
    elif k_to == n:
        return [a + b for a, b in zip(r, r[1:] + [0])]
    else:
        return [a + b for a, b in zip(r[:-1], r[1:])]

def binExt(n, k_from, k_to):
    if n == 0:
        return [1]
    else:
        r = binExt(n-1, max(0, k_from-1), min(n-1, k_to+1) )
        return binExt_(n, k_from, k_to, r)

def bin(n, k):
    return binExt(n, k, k)[0]

def binExtOpt(n, k_from, k_to):
    if n == 0:
        return [1]
    ks = [(k_from, k_to)]
    for i in range(1,n):
        ks.append((max(0, ks[-1][0]-1), min(n-i, ks[-1][1]+1)))
    x = 0
    r = [1]
    while x != n:
        x += 1
        r = binExt_(x, *ks[n-x], r)
    return r

def binOpt(n, k):
    return binExtOpt(n, k, k)[0]

In the end, the most difficult task is not switching from recursive to iterative implementation, but to have a recursive implementation that follows the required pattern. So the real question is how to create fibExt', not fibExtOpt.

Tim Fuchs
  • 1,171
  • 6
  • 15
  • Thx for the answer! Probably I'll be giving the bounty to you cos there aren't any other answers and I don't want it to be wasted... In any case, if you feel like you can complete your answer for the rest of the functions you'll be getting also a validation. Did you understand the proposed method by the way? +1 For the initial attempt. – BPL Aug 24 '16 at 18:44
  • I have found the book [online](https://books.google.de/books?id=CTqjwTnokmYC&pg=PA83&dq=editions:ISBN1107610796&source=gbs_toc_r&cad=4#v=onepage&q=editions%3AISBN1107610796&f=false) and read the chapter by now, so I think I understand the proposed method. But I think your question really does not provide enough information to answer it without checking with the book first. I will extend my answer and maybe propose an edit to the question. – Tim Fuchs Aug 25 '16 at 12:02
  • For the time being the bounty is yours... if you're editing the question and completing answer, of course, I'll also validate your answer, thanks for the effort! – BPL Aug 25 '16 at 13:08
  • Sorry, I've seen your question just now, I see you haven't provided the solution for the whole set of problems but the answer is quite complete so I'll accept it. Thanks for the effort! – BPL Sep 05 '16 at 18:20