The towers of Hanoi problem [1] is a famous puzzle demonstrating recursion. The task is to move a stack of disks from one peg to another. We can only move one disk at a time, and we need to obey the rule that larger disks can never be on top of a smaller disk. The moves for the 3 disk problem are:
Recursive algorithm
# returns number of moves def hanoi(n,A,B,C): if n==0: return 0 n1 = hanoi(n-1,A,C,B) print(f"move disk from peg {A} to {B}") n2 = hanoi(n-1,C,B,A) return n1+1+n2 N = 3 print(f"N={N}") cnt = hanoi(N,'A','B','C') print(f"{cnt} moves")
The three steps to move n disks from peg A→B are:
- Move n−1 disks from A→C
- Move single remaining disk from A→B
- Move n−1 disks from C→B
The results are:
N=3 move disk from peg A to B move disk from peg A to C move disk from peg B to C move disk from peg A to B move disk from peg C to A move disk from peg C to B move disk from peg A to B 7 moves
The number of moves is 2n−1. Indeed, if we run this with 4 disks, we see:
N=4 move disk from peg A to C move disk from peg A to B move disk from peg C to B move disk from peg A to C move disk from peg B to A move disk from peg B to C move disk from peg A to C move disk from peg A to B move disk from peg C to B move disk from peg C to A move disk from peg B to A move disk from peg C to B move disk from peg A to C move disk from peg A to B move disk from peg C to B 15 moves
This solution can be visualized as:
Inventory model
Just as in [2] where we modeled the wolf, goat, cabbage puzzle, we can model this in two ways: a formulation based on keeping track of inventory, and a shortest-path network formulation. Of course, no one in their right mind would solve this as an optimization problem. Obviously, that does not stop us from trying anyway. In an optimization model, we don't prescribe the moves to make, but rather formulate the conditions that should hold. A very different way to look at the problem.
The inventory is here modeled as the state: the location of the disks just after a move. I.e. we have invt,disk,peg∈{0,1} where t is the time index (one move per time period). We ignore here how the disks are organized (ordered from large to small), we just keep track of which peg a disk is assigned to. (In the next paragraphs, we will worry about the constraint that a move can only involve the smallest disks). We could formulate a constraint that says that every disk must be assigned to exactly one peg, a typical assignment constraint: ∑peginvt,disk,peg=1∀t,disk However, it turns out that this is not really necessary. The inventory balance equation takes care of that (given a valid initial inventory). Adding this constraint can help with performance.
The move variable is movet,disk,peg1,peg2∈{0,1} We can only move one disk at a time, so: ∑disk,peg1,peg2movet,disk,peg1,peg2=1∀t Let's also forbid moves from and to the same peg (that is not a real move): movet,disk,peg,peg=0
Like in [2], we allow no moves when we are done: ∑disk,peg1,peg2movet,disk,peg1,peg2=1−donet where donet∈{0,1}.
We need to make sure that the variable donet is 1 if and only if all disks are placed on peg B: ∑diskinvt,disk,pegB≥n⋅donet We rely on the objective to make sure that donet=1 when possible.
The inventory balance equation is a difference equation: invt,disk,peg1=invt−1,disk,peg1−∑peg2movet,disk,peg1,peg2+∑peg2movet,disk,peg2,peg1∀t,disk,peg1
The only thing left is worrying about the size of the disks. We can only move the smallest disk from a peg, and this disk should be smaller than the other disks on the target peg. In our model, we assume that we have n disks with sizes 1,…,n. Just to make things more concrete. We could have used any other scheme. We need to say that: the size of the disk moved is (1) the smallest of the "from" peg, and (2) smaller than any disk on the target peg. For this, we look at the state (inventory) at the end of the previous step. Let's define a continuous variable smallestPrevt,peg={mindisk|invt−1,disk,peg=1sizediskif peg has one or more disksnif peg has zero disks Instead of an equality it is easier to use a bound: smallestPrevt,peg≤{mindisk|invt−1,disk,peg=1sizedisk if peg has one or more disksnif peg has zero disksThis can be implemented as: smallestPrevt,peg≤sizedisk⋅invt−1,disk,peg+n⋅(1−invt−1,disk,peg) With this, we can make sure we move an appropriate small disk: ∑disk,peg2sizedisk⋅movet,disk,peg1,peg2≤smallestPrevt,peg1∀t,peg1∑disk,peg1sizedisk⋅movet,disk,peg1,peg2≤smallestPrevt,peg2∀t,peg2
The last inequality preferably should have a right-hand side that is one less. However, we cannot just subtract one from smallestPrevt,peg2 as smallestPrevt,peg2−1 may forbid moving disks of size n. It is noted that the constraint is correct, as we can never place two disks of the same size on a peg. So, I am a bit lazy here, and just reuse the same smallestPrev for both the source and destination peg.
The objective is simply to maximize the number of "done" iterations: max∑tdonet This is only needed if we overestimate the number iterations we need (the set t is larger than needed). If we use a size of 2n−1 for the set t, we will be done exactly at the last iteration.
The complete model looks like:
Inventory Model |
---|
max∑tdonet∑disk,peg1,peg2movet,disk,peg1,peg2=1−donet∀tmovet,disk,peg,peg=0∀t,disk,peg∑diskinvt,disk,pegB≥n⋅donet∀tinvt,disk,peg1=invt−1,disk,peg1−∑peg2movet,disk,peg1,peg2+∑peg2movet,disk,peg2,peg1∀t,disk,peg1smallestPrevt,peg≤sizedisk⋅invt−1,disk,peg+n⋅(1−invt−1,disk,peg)∀t,disk,peg∑disk,peg2sizedisk⋅movet,disk,peg1,peg2≤smallestPrevt,peg1∀t,peg1∑disk,peg1sizedisk⋅movet,disk,peg1,peg2≤smallestPrevt,peg2∀t,peg2invt,disk,peg∈{0,1}movet,disk,peg1,peg2∈{0,1}donet∈{0,1}smallestPrevt,peg∈[1,…,n] |
The results for a n=3 disk problem are:
---- 157 VARIABLE move.L disk is moved from one peg to another pegA.pegB pegA.pegC pegB.pegC pegC.pegA pegC.pegB t1.disk1 1 t2.disk2 1 t3.disk1 1 t4.disk3 1 t5.disk1 1 t6.disk2 1 t7.disk1 1 ---- 157 VARIABLE inv.L inventory after move pegA pegB pegC t1.disk1 1.000 t1.disk2 1.000 t1.disk3 1.000 t2.disk1 1.000 t2.disk2 1.000 t2.disk3 1.000 t3.disk1 1.000 t3.disk2 1.000 t3.disk3 1.000 t4.disk1 1.000 t4.disk2 1.000 t4.disk3 1.000 t5.disk1 1.000 t5.disk2 1.000 t5.disk3 1.000 t6.disk1 1.000 t6.disk2 1.000 t6.disk3 1.000 t7.disk1 1.000 t7.disk2 1.000 t7.disk3 1.000
The corresponding plot generated from this solution is:
GAMS Model
Shortest path network model
---- 30 SET disk disk1, disk2, disk3 ---- 30 SET peg pegA, pegB, PegC ---- 30 SET node node1 , node2 , node3 , node4 , node5 , node6 , node7 , node8 , node9 , node10, node11, node12 node13, node14, node15, node16, node17, node18, node19, node20, node21, node22, node23, node24 node25, node26, node27 ---- 30 SET nodes pegA pegB PegC node1 .disk1 YES node1 .disk2 YES node1 .disk3 YES node2 .disk1 YES node2 .disk2 YES node2 .disk3 YES node3 .disk1 YES node3 .disk2 YES node3 .disk3 YES node4 .disk1 YES node4 .disk2 YES node4 .disk3 YES node5 .disk1 YES node5 .disk2 YES node5 .disk3 YES node6 .disk1 YES node6 .disk2 YES node6 .disk3 YES node7 .disk1 YES node7 .disk2 YES node7 .disk3 YES node8 .disk1 YES node8 .disk2 YES node8 .disk3 YES node9 .disk1 YES node9 .disk2 YES node9 .disk3 YES node10.disk1 YES node10.disk2 YES node10.disk3 YES node11.disk1 YES node11.disk2 YES node11.disk3 YES node12.disk1 YES node12.disk2 YES node12.disk3 YES node13.disk1 YES node13.disk2 YES node13.disk3 YES node14.disk1 YES node14.disk2 YES node14.disk3 YES node15.disk1 YES node15.disk2 YES node15.disk3 YES node16.disk1 YES node16.disk2 YES node16.disk3 YES node17.disk1 YES node17.disk2 YES node17.disk3 YES node18.disk1 YES node18.disk2 YES node18.disk3 YES node19.disk1 YES node19.disk2 YES node19.disk3 YES node20.disk1 YES node20.disk2 YES node20.disk3 YES node21.disk1 YES node21.disk2 YES node21.disk3 YES node22.disk1 YES node22.disk2 YES node22.disk3 YES node23.disk1 YES node23.disk2 YES node23.disk3 YES node24.disk1 YES node24.disk2 YES node24.disk3 YES node25.disk1 YES node25.disk2 YES node25.disk3 YES node26.disk1 YES node26.disk2 YES node26.disk3 YES node27.disk1 YES node27.disk2 YES node27.disk3 YES
The directed arcs node1→node2 have the following properties:
- The difference between node1 and node2 is just one disk being moved from one peg to another. E.g., there is no arc between node 1 and node 5, as the difference between them is 2.
- The disk being moved is the smallest on its peg, both at node1 and node2. E.g., these is no arc between node 2 and node 5, as disk 2 is not the smallest on peg A in node 2.
If we check this systematically and carefully, we get the following set of feasible moves (and thus arcs):
---- 63 SET diskMoved feasible moves
disk1 disk2 disk3
node1 .node10 YES
node1 .node19 YES
node2 .node3 YES
node2 .node11 YES
node2 .node20 YES
node3 .node2 YES
node3 .node12 YES
node3 .node21 YES
node4 .node7 YES
node4 .node13 YES
node4 .node22 YES
node5 .node8 YES
node5 .node14 YES
node5 .node23 YES
node6 .node9 YES
node6 .node15 YES
node6 .node24 YES
node7 .node4 YES
node7 .node16 YES
node7 .node25 YES
node8 .node5 YES
node8 .node17 YES
node8 .node26 YES
node9 .node6 YES
node9 .node18 YES
node9 .node27 YES
node10.node1 YES
node10.node16 YES
node10.node19 YES
node11.node2 YES
node11.node17 YES
node11.node20 YES
node12.node3 YES
node12.node18 YES
node12.node21 YES
node13.node4 YES
node13.node15 YES
node13.node22 YES
node14.node5 YES
node14.node23 YES
node15.node6 YES
node15.node13 YES
node15.node24 YES
node16.node7 YES
node16.node10 YES
node16.node25 YES
node17.node8 YES
node17.node11 YES
node17.node26 YES
node18.node9 YES
node18.node12 YES
node18.node27 YES
node19.node1 YES
node19.node10 YES
node19.node22 YES
node20.node2 YES
node20.node11 YES
node20.node23 YES
node21.node3 YES
node21.node12 YES
node21.node24 YES
node22.node4 YES
node22.node13 YES
node22.node19 YES
node23.node5 YES
node23.node14 YES
node23.node20 YES
node24.node6 YES
node24.node15 YES
node24.node21 YES
node25.node7 YES
node25.node16 YES
node25.node26 YES
node26.node8 YES
node26.node17 YES
node26.node25 YES
node27.node9 YES
node27.node18 YES
The number of directed arcs is known [3]: 3⋅(3n−1), so we have an easy check if our procedure to generate arcs is correct. For n=3 the number of arcs is 78.
To solve this model, we use a standard LP formulation:
Shortest path LP Model |
---|
min∑(i,j)∈Afi,j∑j|(j,i)∈Afj,i+supplyi=∑j|(i,j)∈Afi,j+demandi∀ifi,j≥0supplyi={1if i is the source node0otherwisedemandi={1if i is the sink node0otherwise |
The raw results are:
---- 93 VARIABLE f.L flow node5 node8 node10 node14 node16 node25 node26 node1 1.000 node5 1.000 node8 1.000 node10 1.000 node16 1.000 node25 1.000 node26 1.000
A more meaningful report created from this solution is:
---- 125 SET trace
move1.node1 .node10.disk1.pegA.pegB
move2.node10.node16.disk2.pegA.PegC
move3.node16.node25.disk1.pegB.PegC
move4.node25.node26.disk3.pegA.pegB
move5.node26.node8 .disk1.PegC.pegA
move6.node8 .node5 .disk2.PegC.pegB
move7.node5 .node14.disk1.pegA.pegB
GAMS Model
Conclusion
Interestingly, we could use the same strategies we previously applied for the Wolf, Goat, and Cabbage problem [2]: a MIP model where the dynamics are handled by an inventory balance equation and a shortest path model.
From a performance point of view, the network approach is much better: shortest path problems are easy LPs, and we can even use specialized algorithms if we want. The MIP model is not so easy when we increase the size of the problem a bit. Interestingly, the open source solver HiGHS seems to do better than Cplex on this particular model (caveat: I only ran a few tests). It is also noted that the implementations of both models (MIP and network model) are not carefully optimized for speed. I just wanted to demonstrate the feasibility of these approaches for this problem. The shortest path approach has been suggested by others, so that is not my idea.
I believe these models are a bit more general than the recursive algorithm: we can start from any given configuration, while the recursion can only start with all disks on one peg. (Similar for the final configuration: we are flexible about this.)
References
- Tower of Hanoi, https://en.wikipedia.org/wiki/Tower_of_Hanoi
- Wolf, goat, and cabbage problem: MIP and Network Model, https://yetanothermathprogrammingconsultant.blogspot.com/2025/03/wolf-goat-and-cabbage-problem-mip-and.html
- Hanoi graph, https://en.wikipedia.org/wiki/Hanoi_graph
HiGHS is quite formidable. While I would be shocked if we see it outperform commercial solvers on standard benchmark sets like MIPLIB20XX, it is absolutely worth running a comparison to see how it performs on a specific problem vs a commercial solver - you may find that the free solution is faster and better.
ReplyDelete