-3

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.

KramerDK
  • 15
  • 7
  • 1
    What do you mean when you say it doesn't 'work' - does it give the wrong list of moves, or does it throw an error? – alexhroom Mar 26 '23 at 13:42
  • The problem with the code is: I don't see the idea of the hanoi solution. The idea is to move N-1 plates to a pole, then move plate N to the destination and then move N-1 plates to the destination. I am not able to recognize that pattern. Solver() should really be just 2 calls to itself. – Thomas Weller Mar 26 '23 at 13:42
  • It throws a recursion error for num_disks >= 4 – KramerDK Mar 26 '23 at 13:43
  • You implemented this 2 days ago and you can't tell us why it works for 1, 2 and 3 disks? If you don't understand your code that much, you should probably start over from scratch. This is a really trivial algorithm which can be implemented in 8 lines of Python code in 4 minutes (I just did it, that's why I know). There's no need spending 30 minutes of analyzing this overcomplicated solution – Thomas Weller Mar 26 '23 at 13:52
  • Moving one plate from one pole to another does not require a recursion. You can just do it. It's only one plate – Thomas Weller Mar 26 '23 at 13:53
  • *"it gives the right moves for exactly the first 10 steps"* - What are those first 10 steps when you only have one disk? – Kelly Bundy Mar 26 '23 at 13:56
  • @KellyBundy 'The first 10 steps' are those when there atleast as many required steps in solving the problem, total number of steps = 2^n-1 – KramerDK Mar 26 '23 at 13:59
  • @KellyBundy tried running the code? – KramerDK Mar 26 '23 at 14:02
  • 2
    Don't debug. Rewrite from scratch. It's not worth it. – Thomas Weller Mar 26 '23 at 14:05
  • @NagarajuChukkala Yes. And I only saw one move. So I was wondering which ten moves you saw, since you said you did. – Kelly Bundy Mar 26 '23 at 14:05
  • Ok then what are the first ten moves you get for 4 disks? As you said and as I saw myself, your program crashes with an error, not giving any moves. – Kelly Bundy Mar 26 '23 at 15:03

1 Answers1

2

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)
trincot
  • 317,000
  • 35
  • 244
  • 286