Recursive Staircase in Picolisp
rick, 2019-05-18

Recursive staircase

Background

Apparently this is a problem employers ask, See these youtube vids to get an idea.

The host of the first video found the statement of the problem at https://www.dailycodingproblem.com. Here it is.

There’s a staircase with N steps, and you can climb 1 or 2 steps at a time. Given N, write a function that returns the number of unique ways you can climb the staircase. The order of the steps matters.

For example, if N is 4, then there are 5 unique ways:

  • 1, 1, 1, 1
  • 2, 1, 1
  • 1, 2, 1
  • 1, 1, 2
  • 2, 2

What if, instead of being able to climb 1 or 2 steps at a time, you could climb any number from a set of positive integers X? For example, if X = {1, 3, 5}, you could climb 1, 3, or 5 steps at a time. Generalize your function to take in X.

Picolisp solution

Yeah, don’t use Java, as in one of the referenced videos above (boy, was that painful to watch). We’ll be using picolisp1. Let’s look at a recursive solution; then, let’s see how we can improve upon it.

Recursive solution

Our recursive solution here computes the number of ways to climb a staircase of Total-steps steps, given that one can take strides of various sizes (found in the input list Strides).

(de num-ways (Total-steps Strides)
  (sum '((Stride)
         (let Remaining (- Total-steps Stride)
              (cond ((lt0 Remaining) 0)
                    ((=0  Remaining) 1)
                    (T (num-ways Remaining Strides)))))
       Strides))

Uses: de, sum, let, -, cond, lt0, =0, T.

For each stride Stride, the number of ways to climb the entire staircase starting with taking a step of length Stride is equal to the number of ways to climb the remaining steps that you see after you take that initial step (namely, (num-ways Remaining Strides)). That’s how we reduce the problem to sub-problems of a smaller size. When these number of ways (for each (starting) stride) are computed, they are added together via the outer call to sum. The base cases of the recursion have these conditions:

Example

Question: how many ways can one climb a 10-step staircase, if that person could climb one step at a time or two steps at a time (but no more)? Answer: (num-ways 10 (1 2)) -> 89 ways.

Notes

Notice that I used Total-steps (instead of N) and Strides (instead of X). I usually prefer longer, meaningful symbol names for identifiers, but shorter names sometimes yield easier-to-read code. It’s a judgment call and a matter of taste. (“Yeah, well, you know, that’s just, like, your opinion, man.”) Know your reader, if possible.

Some assumptions are being made:

This, of course, may be enforced with extra code, and in a production environment it should be.

But more importantly, notice the recursive call. With a large enough value for Total-steps, we are going to blow up our call stack. So, let’s see about turning that into a loop.

Non-recursive solution

First, we observe that we are computing a sequence of numbers not unlike the Fibonacci sequence where we compute the I-th term of the sequence as the sum of the previous J-th terms (already computed) for JStrides. 2

Here’s the “bottom up” solution method described by the first video (toward the end) ported to picolisp. Notice how the picolisp code is no less concise than the pseudo/Python code from that video.

(de num-ways2 (Total-steps Strides)
  (let (Previous (list 1) Result 0)
    (for I Total-steps
      (zero Result)
      (for J Strides
        (when (>= I J)
          (inc 'Result (get Previous J))))  # add in the J-th preceding term
      (push 'Previous Result))              # cache the I-th term
    Result))

Uses: de, let, list, for, zero, when, inc, get, push.

We can now compute the answer for large Total-steps.

: (num-ways2 400 (1 2))
-> 284812298108489611757988937681460995615380088782304890986477195645969271404032323901
: (num-ways2 4000 (1 2))
-> 64574884490948173531376949015369595644413900640151342708407577598177210359034088914449477807287241743760741523783818897499227009742183152482019062763550798743704275106856470216307593623057388506776767202069670477506088895294300509291166023947866841763853953813982281703936665369922709095308006821399524780721049955829191407029943622087779296459174012610148659520381170452591141331949336080577141708645783606636081941915217355115810993973945783493983844592749672661361548061615756595818944317619922097369917676974058206341892088144549337974422952140132621568340701016273422727827762726153066303093052982051757444742428033107522419466219655780413101759505231617222578292486081002391218785189299675757766920269402348733644662725774717740924068828300186439425921761082545463164628807702653752619616157324434040342057336683279284098590801501
:

In fact, we are only limited by two things, it appears: (i) the size of an integer (we have bignums here) and (ii) the size of the internal cache Previous which grows linearly with respect to Total-steps.

Columbo ending (“Just one more thing …”)

We can make the function have constant space by limiting the length of the cache Previous. After all, we only have to “remember” as far back as the M-th previous term, where M is (max Strides). For instance, if Strides is (1 3 5), we only have to remember as far back as 5 “terms ago” – 6 terms ago, 7 terms ago, and so on, we don’t care about any more. Accordingly, we have the following.

(de num-ways3 (Total-steps Strides)
  (let (Previous (need (- (apply max Strides)) (list 1) 0)
        Result   0)
    (do Total-steps
      (zero Result)
      (for J Strides
        (inc 'Result (get Previous J)))     # add in the J-th preceding term
      (set (rot Previous) Result))          # cache the I-th term
    Result))

Uses: de, let, need, -, apply, list, do, zero, for, inc, get, set, rot.

We took the time to add a couple of improvements: (i) removing the check (when (>= I J), as it’s no longer needed because the cache is padded out with zeros and so we’ll always find a J-th previous entry, even from the beginning of the process, and (ii) removing the index I as it is no longer needed.

Recursive Columbo (“Just one more thing on that ‘one more thing’…”)

Thanks to some very handy primitives in picolisp and the fact that, because lisp programs are expressions (“turtles”) “all the way down,” we can manipulate the code very much like a mathematician manipulates mathematical expressions algebraically. We can “rearrange” or “reduce” the code, while maintaining its equivalence to previous code. In this case, we were able to “cancel out” the “variable” Result. Now, we have the following.

(de num-ways4 (Total-steps Strides)
  (let Previous (need (- (apply max Strides)) (list 1) 0)
    (do Total-steps
      (set (rot Previous) (sum '((J) (get Previous J)) Strides)))
    (car Previous)))

Uses: de, let, need, -, apply, list, do, set, rot, sum, get, car.

The code is shorter (even than the Python code in the first video). It might not be as clear as the code of num-ways3, but if the reader were an experienced picolisp programmer, it would likely be more clear (solely because the body of the function is more concise and idiomatic). The expression (sum '((J) (get Previous J)) Strides) computes the sum of the J-th previous terms (for all J in Strides). This is much more compact than the previous for loop and likely faster. That computation is always stored in the CAR of Previous (in every iteration of the do loop); so, when the process is complete, the final result can be found there.


  1. For readers unfamiliar with picolisp, consult the tutorial (https://software-lab.de/doc/tut.html) which has links in the first two paragraphs for preliminary documents as well as the language reference. [return]
  2. Actually, this function is in fact a generalization of a Fibonacci function. Note that (num-ways N (1 2)) yields the N-th Fibonacci number. Indeed, any function that computes recursive staircase solutions is a generalization of any function that computes the Fibonacci sequence. [return]