<<   >>

38. Tower of Hanoi

Introduction

The Tower of Hanoi problem is to move a set of n different-sized disks from one peg to another, moving one disk at a time, using an intermediate peg if necessary. At all times no larger disk may sit on top of a smaller disk.

For example, moving 3 disks from peg 0 to peg 1 can be done as follows:

  • move disk 0 from peg 0 to peg 1
  • move disk 1 from peg 0 to peg 2
  • move disk 0 from peg 1 to peg 2
  • move disk 2 from peg 0 to peg 1
  • move disk 0 from peg 2 to peg 0
  • move disk 1 from peg 2 to peg 1
  • move disk 0 from peg 0 to peg 1

The description of the moves can be shortened if we observed that in moving a disk from peg A to peg B, it is always the top disk on peg A that is moved. Thus the 3-disk problem can be solved as follows:

0 1
0 2
1 2
0 1
2 0
2 1
0 1

Legend has it that a group of priests has been solving the 64-disk problem since the beginning of time, and when they finish, the world will come to an end.

An Initial Solution

There is a simple recursive solution to the n-disk problem: First, move disks 0 to n-2 from peg 0 to peg 2 , then move disk n-1 from peg 0 to peg 1 , then move disks 0 to n-2 from peg 2 to peg 1 . If there is just one disk, the one disk can be moved from peg 0 to peg 1 straightaway.

This can be implemented as an ambivalent function: the right argument is the number of disks; the left argument are 3 integers of the pegs (from, to, via), with a default of 0 1 2 .

H←{
  ⍺←0 1 2
  0=⍵:0 2⍴0
  (⍺[0 2 1] ∇ ⍵-1)⍪(2↑⍺)⍪(⍺[2 1 0] ∇ ⍵-1)
}

   H 3
0 1
0 2
1 2
0 1
2 0
2 1
0 1
From the definition of H , it is easy to see that the n-disk problem requires ¯1+2*n moves. If n=0 , there are 0 moves (0=¯1+2*0). If n=k-1 requires ¯1+2*k-1 moves, then n=k requires the following numbers of moves:
 ¯1+2*k-1    (⍺[0 2 1] ∇ ⍵-1)
 1           (2↑⍺)
 ¯1+2*k-1    (⍺[2 1 0] ∇ ⍵-1)

for a total of ¯1+2*k moves. A similar argument shows that the n-disk problem requires ¯1+2*n calls of the function H .

   ≢∘H¨ ⍳10
0 1 3 7 15 31 63 127 255 511
   ¯1 + 2 * ⍳10
0 1 3 7 15 31 63 127 255 511

Singly Recursive Solutions

The two recursive steps in H differ only in the labelling of the pegs. Therefore we can replace them with a single recursive call, and do two relabellings of the pegs to get the effect of two recursions. That is function H1 below.

Moreover, in verb H1 the left argument is unchanged in the recursion; that is, the left argument is always 0 1 2 , and can be elided.

We saw previously that on the n-disk problem the doubly recursive H required ¯1+2*n calls. In the singly recursive H1 and H2 , the n-disk problem requires n calls. Benchmarks on the 10-disk problem demonstrate the difference this makes.

H1←{⍺←0 1 2 ⋄ 0=⍵:0 2⍴0 ⋄ 0 2 1[t]⍪0 1⍪2 1 0[t←⍺ ∇ ⍵-1]}

H2←{0=⍵:0 2⍴0 ⋄ 0 2 1[t]⍪0 1⍪2 1 0[t←∇ ⍵-1]}

   (H ≡ H1)¨ ⍳10
1 1 1 1 1 1 1 1 1 1
   (H ≡ H2)¨ ⍳10
1 1 1 1 1 1 1 1 1 1

Non-Recursive Solutions

We now proceed to derive non-recursive solutions to the problem.

The results of verbs H , H1 , and H2 are rows of the 6-row matrix A (the 6 different ways of moving from peg i to peg j where i and j can be 0 , 1 , or 2). Thus a more efficient representation for the solutions is to encode the moves as row indices of A , the integers from 0 to 5 .

   A← 6 2 ⍴ 0 1 0 2 1 0 1 2 2 0 2 1
   A
0 1
0 2
1 0
1 2
2 0
2 1

   A ⍳ H2 1
0
   A ⍳ H2 2
1 0 5
   A ⍳ H2 3
0 1 3 0 4 5 0
   A ⍳ H2 4
1 0 5 1 2 3 1 0 5 4 2 5 1 0 5
   A ⍳ H2 5
0 1 3 0 4 5 0 1 3 2 4 3 0 1 3 0 4 5 0 4 3 2 4 5 0 1 3 0 4 5 0

Judicious alignment of the indices reveals a pattern:

1:   0
2: 1 0 5

2:   1   0   5
3: 0 1 3 0 4 5 0

3:   0   1   3   0   4   5   0
4: 1 0 5 1 2 3 1 0 5 4 2 5 1 0 5

4:   1   0   5   1   2   3   1   0   5   4   2   5   1   0   5
5: 0 1 3 0 4 5 0 1 3 2 4 3 0 1 3 0 4 5 0 4 3 2 4 5 0 1 3 0 4 5 0

To get from A ⍳ H2 n-1 to A ⍳ H2 n , intersperse the result for n with 1 5 2 if n is even and with 0 3 4 if n is odd. Thus:

H3←{0=⍵:⍬ ⋄ ¯1↓,((2*⍵-1)⍴(2|⍵)⌷2 3⍴1 5 2 0 3 4),⍪0,⍨∇ ⍵-1}

   H3 3
0 1 3 0 4 5 0
   (A ⍳ H2 3) ≡ H3 3
1
   (A∘⍳∘H2 ≡ H3)¨ ⍳10
1 1 1 1 1 1 1 1 1 1

H3 n works by appending a scalar to the result of H3 n-1 ; then stitching a list, repetitions of 1 5 2 or 0 3 4 ; then ravelling; then dropping the last (previously appended) scalar.

The list of numbers to be stitched (repetitions of 0 3 4 or 1 5 2) depends only on n and on H3 n-1 not at all. This suggests a different method of directing the computation: Compute the lists xi of numbers to be stitched for 1 , 2 , 3 , …, up to n , and then apply an appropriate function f between them:

xn f ... f x3 f x2 f x1
⊃ f / xn ...  x3 x2 x1

Moreover, we can avoid incorporating into f the appending and dropping of scalar, if we start out with an “extra” scalar and drop it only at the end:

⊃ f / xn ... x3 x2 x1
¯1 ↓ ⊃ g / xn ... x3 x2 x1 atom

In H4 below, the function g is (, ,⍤0) (ravel stitch).

H4←{¯1↓⊃(,,⍤0)/⍵,⍨(⌽2*⍳⍵)⍴¨⌽⍵⍴(0 3 4)(1 5 2)}

   (H3 ≡ H4)¨ ⍳10
1 1 1 1 1 1 1 1 1 1

   arg←{⍵,⍨(⌽2*⍳⍵)⍴¨⌽⍵⍴(0 3 4)(1 5 2)}
   arg 1
┌─┬─┐
│0│1│
└─┴─┘
   arg 2
┌───┬─┬─┐
│1 5│0│2│
└───┴─┴─┘
   arg 3
┌───────┬───┬─┬─┐
│0 3 4 0│1 5│0│3│
└───────┴───┴─┴─┘
   arg 4
┌───────────────┬───────┬───┬─┬─┐
│1 5 2 1 5 2 1 5│0 3 4 0│1 5│0│4│
└───────────────┴───────┴───┴─┴─┘
   arg 5
┌───────────────────────────────┬───────────────┬
│0 3 4 0 3 4 0 3 4 0 3 4 0 3 4 0│1 5 2 1 5 2 1 5│...
└───────────────────────────────┴───────────────┴
                                    ┬───────┬───┬─┬─┐
                                 ...│0 3 4 0│1 5│0│5│
                                    ┴───────┴───┴─┴─┘

There is another possibility. The pattern in H4 is:

¯1 ↓ ⊃ g / xn ... x3 x2 x1 atom

In an intermediate stage, when g is being applied,

xi g yi

g “knows” from yi alone what xi has to be: the length of yi determines the length of xi , and the leading scalar of yi (0 or 1) determines whether 1 5 2 or 0 3 4 should be ravel-stitched. This suggests another solution: a monad is applied n times to the vector ,1 .

H5←{¯1↓{⍵(,,⍤0)⍨(≢⍵)⍴(⊃⍵)⊃(1 5 2)(0 3 4)}⍣⍵,1}

   (H4 ≡ H5)¨ ⍳10
1 1 1 1 1 1 1 1 1 1

   rsa←{⍵(,,⍤0)⍨(≢⍵)⍴(⊃⍵)⊃(1 5 2)(0 3 4)}
   rsa 1
0 1
   rsa rsa 1
1 0 5 1
   rsa rsa rsa 1
0 1 3 0 4 5 0 1
   rsa⍣3 ,1
0 1 3 0 4 5 0 1
   H5 3
0 1 3 0 4 5 0

Conclusion

The Tower of Hanoi problem can be solved in a variety of ways, with a wide variation in efficiency.

  • double recursion
  • H1 single recursion
  • H2 single recursion monad
  • H3 single recursion, scalar representation
  • H4 non-recursive, reduction
  • H5 non-recursive, power


Appeared in J in [107, 108].