Recursive Python code for Tower of Hanoi works for up to 3 disks, but not more

Question:

I am trying to implement Tower of Hanoi using recursion with my own approach. I don’t know if it is a valid solution at all. But it definitely works 1, 2, and 3 disks. In fact, given a number of disks more than 3, it gives the right moves for exactly the first 10 steps. I tried but couldn’t figure it out. I want to know what is wrong with my code, and I appreciate it if you could spend some of your time on it.


#The `solver` tries to modify the `moves` global variable to include the correct sequence of 
#moves to be made. Example output: moves -> [(0,1), (0,2), (1,2)] for 2 disks (num_disks = 2)

def solver (start, source, target):
    if start[source] == 1:
        start[source] -= 1
        start[target] += 1
        moves.append((source, target))
        return start
    else:
        start[source] -= 1
        target = 3 - (source+target)
        start = solver(start, source, target)
        start[source] += 1 
        target = 3 - (source+target)
        start = solver(start, source, target)
        source = 3 - (source+target)
        start = solver(start, source, target)
        return start


def convert_to_stack_sizes_and_pass_to_solver (start, source, target):
    stack_sizes = [len(start[0]), len(start[1]), len(start[2])]
    solver(stack_sizes, source, target)

num_disks = 3
all_disks = list(range(0, num_disks))
moves = []
starting_stacks = [all_disks, [], []]
convert_to_stack_sizes_and_pass_to_solver (starting_stacks, 0, 2)
print(moves)

#Please look at 'moves' variable for output, after execution.

Tried debugging with VS code, but recursion is too counter-intuitive for me for that. You can run the code as is and get the output for up to 3 disks.

Asked By: Nagaraju Chukkala

||

Answers:

The idea you have is fine, but when we look at the recursive part, we can see the idea is not fully "mirrored". The core of your idea is to do start[source] -= 1 to indicate that one less disc should move by the recursive call. After this call this is restored. However, this idea is not repeated for the second half of the recursion.

To explain, let’s first see where things go wrong:

The problem with 4 discs can be pictured as follows:

   ▄▄▄ 
  ▄▄▄▄▄ 
 ▄▄▄▄▄▄▄ 
▄▄▄▄▄▄▄▄▄ 
    ┴          ┴          ┴

All goes well to transfer the top three discs to the second pile:

              ▄▄▄
             ▄▄▄▄▄
▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄
    ┴          ┴          ┴

And the transfer of the greatest disc goes well too:

              ▄▄▄
             ▄▄▄▄▄
            ▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄
    ┴          ┴          ┴

Then it rightly aims to move the top two discs of the middle pile to the left, and for that it needs to move the top one to the right:

             ▄▄▄▄▄       ▄▄▄
            ▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄
    ┴          ┴          ┴

…and the next one to the left:

                         ▄▄▄
  ▄▄▄▄▄     ▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄
    ┴          ┴          ┴

And now things go wrong. The smallest disc should move to the first pile, but because the size of the third pile is not 1, but 2, this is not correctly recognised. Instead a logic is applied as if the two rightmost discs must move.

The correction

We would want to indicate that only 1 disc has to move, and for this to happen we should mirror the action we took on the source side to the target side. The change is simple — adding 2 lines that mirror the change made in the first half of the else block:

def solver (start, source, target):
    if start[source] == 1:
        start[source] -= 1
        start[target] += 1
        moves.append((source, target))
        return start
    else:
        start[source] -= 1
        target = 3 - (source+target)
        start = solver(start, source, target)
        start[source] += 1 
        target = 3 - (source+target)
        start = solver(start, source, target)
        start[target] -= 1  # Add this
        source = 3 - (source+target)
        start = solver(start, source, target)
        start[target] += 1  # ...and this
        return start

Once you see it, it looks evident.

Other remarks

You should better avoid the use of a global variable to collect the moves. In fact, you could let the solver yield the moves, and then the caller can collect those into a list (if desired).

Like this:

def solver (start, source, target):  # generator
    if start[source] == 1:
        start[source] -= 1
        start[target] += 1
        yield source, target
    else:
        start[source] -= 1
        yield from solver(start, source, 3 - (source+target))
        start[source] += 1 
        yield from solver(start, source, target)
        start[target] -= 1
        yield from solver(start, 3 - (source+target), target)
        start[target] += 1
        

def convert_to_stack_sizes_and_pass_to_solver (start, source, target):
    # Get the yielded values into a new list, and return it
    return list(solver(list(map(len, start)), source, target))

# Main 
num_disks = 6
all_disks = list(range(num_disks))
starting_stacks = [all_disks, [], []]
# Capture the returned list
moves = convert_to_stack_sizes_and_pass_to_solver(starting_stacks, 0, 2)
print(moves)
Answered By: trincot
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.