Решение TweetMazes с рекурсивным возвратом в Python — запрос на помощь по отладке

Я изучаю Python в качестве первого шага к тому, чтобы понять, хочу ли я полностью сменить профессию преподавателя на какую-то разработку программного обеспечения. Пока все идет хорошо, и я работаю над небольшими проектами, чтобы действительно расширить свои возможности. Недавно я провел несколько TweetMaze со своими учениками и понял, что создание программы на Python для решения этих лабиринтов было бы для меня отличной провокацией. TweetMazes — это одномерные лабиринты с перескакиванием чисел. Здесь они хорошо описаны. Почти сразу же я понял, что способ, которым люди решают эти лабиринты, заключается в реализации рекурсивного поиска с возвратом в их мозгу, поэтому я решил создать алгоритм рекурсивного поиска с возвратом для их решения. Я нахожу рекурсию увлекательной, а также очень трудной для понимания. Примерно через неделю возни с кодом я получил что-то, что одновременно работает и не работает, с чем я не могу совладать, поэтому я надеюсь, что кто-нибудь поможет мне понять, что происходит.

Во-первых, пример TweetMaze. Это короткая версия, которую я придумал для проверки своего кода. Это список из 6 чисел:

[4, 2, 2, 2, 3, 0]

Вы начинаете с индекса 0, который имеет значение 4. Вы пытаетесь добраться до последнего элемента в списке, который является единственным со значением 0. Значение каждого элемента в списке говорит вам, сколько пространства вы можете прыгать влево или вправо. Итак, вы бы решили лабиринт следующим образом:

1. starting at index 0, there's a value of 4: I can't move left, so I have to jump 4 spaces right to index 4.
2. Index 4 has a value of 3. I can't jump right, that's outside the puzzle, so I have to go left 3 spaces to index 1.
3. Again, I can only go right, so I move 2 spaces right to index 3.
4. Index 3 has a value of 2, and I can jump left or right. Pretty obviously, I jump right 2 spaces to index 5, 
   solving the puzzle. If I had instead gone left, I would have entered a loop.

Я мог бы представить это решение как еще один список, указывающий позиции индекса, занятые в решении. Итак, для приведенной выше головоломки то, что я назвал стеком решений в своей программе на Python, будет:

Solution = [0, 4, 1, 3, 5]

Я также сделал функцию для отображения решения в виде серии символов «#» под лабиринтом, чтобы показать предпринятые шаги. Это должно выглядеть так для приведенного выше лабиринта:

    [4, 2, 2, 2, 3, 0]
     #
                 #
        #
              #
                    #

Итак, вот где все становится странно. Если я запускаю пример лабиринта через свой код, все работает, и я получаю ожидаемый результат. Однако это очень короткий лабиринт, и его легко решить, поэтому для дальнейшего тестирования моего кода я создал новые лабиринты с 7 элементами вместо 6. Я запустил следующий лабиринт, и код завершился и вел себя так, как будто он имел решение, но это совершенно недопустимое решение:

    Maze: [3, 5, 3, 1, 4, 4, 0]        Solution: [0, 3, 4, 2, 5, 1, 6]
           #
                    #
                       #
                 #
                          #
              #
                             #

Я надеюсь, что это все ясно до сих пор. Алгоритм дает сбой довольно рано.

1. It starts at index 0, and moves right by 3 to index 3. That's correct.
2. It moves right by 1 to index 4. That's a valid move, but that's not how the solution should go. As 
   far as I can tell, there's only one solution, and it moves left by 1 to index 2 at this point. 
   However, the move the program made is at least valid still, so I'll move on to its next step.
3. It's at index 4, which as a value of 4. That means it should move left or right by 4, but it doesn't. 
   It moves left by 2 to index 2. It's totally broken down at this point, and has made an invalid move 
   of the wrong step size. I'm absolutely baffled as to how it does this at all. 
   I just don't see in my code how it could move with the wrong step size.
4. From this point, however, it's weirdly back on track and finishes the rest of the maze, 
   making moves of the correct step size to get to the end. It's like it just inserted a wrong step
   and then carried on from the correct point. So basically, what on Earth is happening?

Для наглядности вот как должно было выглядеть решение:

    Maze: [3, 5, 3, 1, 4, 4, 0]        Solution: [0, 3, 2, 5, 1, 6]
           #
                    #
                 #
                          #
              #
                             #

В этот момент все становится еще более странным, потому что я решил добавить список решений_шагов, который выводит то, что алгоритм делает на каждом уровне рекурсии в текстовом формате, чтобы я мог попытаться увидеть, что он делает, и эти шаги решения на самом деле правильный! Они не соответствуют выходным данным списка решений или отображению решения '#'! Я скопирую и вставлю вывод:

    Current position: 0. Stepping right by 3 to index 3.
    Current position: 3. Stepping left by 1 to index 2.
    Current position: 2. Stepping right by 3 to index 5.
    Current position: 5. Stepping left by 4 to index 1.
    Current position: 1. Stepping right by 5 to index 6.

    Solution stack shows these positions: [0, 3, 4, 2, 5, 1, 6]

    [3, 5, 3, 1, 4, 4, 0]
     #
              #
                 #
           #
                    #
        #
                       #

Если вы будете следовать инструкциям, как они написаны, это правильное решение! Но стек решений и отображение # оба неверны, показывая этот странный дополнительный шаг и неправильный ход. Это не единственная головоломка, в которой это происходит, так как я проверял это на других головоломках из 7 элементов и больше. Я потратил около недели на настройку своего пути к чему-то подобному с помощью рекурсивного поиска с возвратом, и, наконец, нахожусь в точке, где я просто не могу сказать, что происходит, поэтому я надеюсь, что кто-то может объяснить это странное поведение. Честно говоря, я очень горжусь тем, как далеко это зашло, потому что я постоянно сталкивался со странными проблемами превышения максимальной глубины рекурсии и сумел их преодолеть. Я хотел бы получить это полностью функциональным, хотя и понять, почему это идет не так. Более того, мне нравится узнавать, как вы точно определяете, что идет не так. Я все еще очень новичок в инструментах и ​​методах отладки, и у меня довольно много проблем с пониманием того, как следовать потоку программы через рекурсивные итерации. Я разместил свой код на GitHub, но, поскольку я все еще новичок в использовании Github, я также опубликую свой код ниже. Большое спасибо!

maze_string = '3531440'

'''Copy and paste maze strings into the above to change which maze is being run.
Other maze strings to play with:
422230
313210
313110
7347737258493420
'''

maze = [int(i) for i in maze_string]

solution = [0]  #this is the solution stack. append to put on new steps, 
    #pop() to backtrack.
    #algorithm doesn't work if I don't initialize the solution list with the 0 position. Hits max recursion depth.

solution_steps = []

def valid(position, direction):
    #pass in the current position, desired direction of move
    global maze
    if direction == 'right':
        return True if position + maze[position] < len(maze) else False
    elif direction == 'left':
        return True if position - maze[position] >= 0 else False
    
def solve(position):
    global solution
    global maze
    
    #if maze is solved, return True
    if maze[position] == 0:
        return True
    
    #try branches  
    if valid(position, 'right') and (position + maze[position] not in solution):
        new = position + maze[position]
        solution.append(new)
        if solve(new):
            solution_steps.append(f'Current position: {position}. Stepping right by {maze[position]} to index {new}.')
            return True
        solution.pop()
        
    if valid(position, 'left') and (position - maze[position] not in solution):
        new = position - maze[position]
        solution.append(new)
        if solve(new):
            solution_steps.append(f'Current position: {position}. Stepping left by {maze[position]} to index {new}.')
            return True
        solution.pop()
    
    solution.append('no solution')
    return False

def display_solution():
    global maze
    global solution
    print(maze)
    for i in solution:
        point = ' ' + 3*i*' ' + '#'
        print(point)
        
if __name__ == "__main__":
    
    solve(0)
    
    if solution != 'no solution':
        for step in reversed(solution_steps):
            print(step)
        print(f'\nSolution stack shows these positions: {solution}\n')   
        display_solution()
    else:
        print(solution)

person The Mountain Nautilus    schedule 17.05.2021    source источник
comment
Добро пожаловать в СО! Обратите внимание, что проблема заключается в Jump Game на Leetcode. Вы можете найти решения или проверить Jump Game in recursion для решения этого и JG II . Этот подход слишком сложен и не требует большого количества кода. В основном отслеживайте, где вы были, и рекурсивно выполняйте обе ветки на каждом шаге, просматривая/обрезая промежуточные результаты. Пожалуйста, всегда избегайте global -- используйте параметры и возвращаемые значения.   -  person ggorlen    schedule 17.05.2021
comment
Спасибо за подсказку к Jump Game! Это поставило меня на большой путь чтения. Эта проблема почти точно такая же, как Jump Game III, так что было очень полезно узнать о ней. Я все еще очень смущен плохими результатами, которые дает мой алгоритм, и не смог понять, почему он создает этот дополнительный шаг. Но теперь я также выясняю, как оптимизировать решение этой проблемы и делать такие вещи, как мемоизация, так что большое спасибо!   -  person The Mountain Nautilus    schedule 21.05.2021