Как ускорить рекурсивный алгоритм

Я пытаюсь решить задачу Hackerrank Игра Камней, (сокращенная) постановка задачи которого скопирована ниже.

enter image description here

Я придумал следующее решение:

# The lines below are for the Hackerrank submission
# T = int(raw_input().strip())
# ns = [int(raw_input().strip()) for _ in range(T)]

T = 8
ns = [1, 2, 3, 4, 5, 6, 7, 10]

legal_moves = [2, 3, 5]

def which_player_wins(n):
    if n <= 1:
        return "Second"               # First player loses
    elif n in legal_moves:
        return "First"                # First player wins immediately
    else:
        next_ns = map(lambda x: n - x, legal_moves)
        next_ns = filter(lambda x: x >= 0, next_ns)
        next_n_rewards = map(which_player_wins, next_ns)      # Reward for opponent
        if any(map(lambda x: x=="Second", next_n_rewards)):            # Opponent enters a losing position
            return "First"
        else:
            return "Second"

for n in ns:
    print which_player_wins(n)

алгоритм по существу является алгоритмом минимакса, поскольку он выглядит на один шаг вперед, а затем рекурсивно вызывает ту же функцию. Проблема в том, что в Hackerrank он заканчивается из - за тайм-аут:

enter image description here

действительно, я заметил, что оценки which_player_wins(40) уже занимает ~2 секунды. Есть идеи для более быстрого решения, которое не будет тайм-аут?

3 ответов


из описания проблемы кажется, что вы можете сохранить промежуточные и окончательные результаты каждого расчета для использования в последующих расчетах. Если это так, рекурсивный алгоритм не является оптимальным. Вместо этого используйте динамический стиль программирования.

другими словами, сохраните глобальный массив, в котором хранятся результаты предыдущих определений выигрышей и проигрышей. Когда вы определяете для нового значения n, вместо того, чтобы рекурсировать весь путь до конца, используйте предыдущий определения.

например, когда вы доберетесь до n=10, вы видите, что первый игрок, удаляющий 3 камня, оставляет 7 камней, которые вы уже видели как выигрыш для второго игрока. Таким образом, 10 камней-это выигрыш для первого игрока.

Я считаю, что ваш рекурсивный алгоритм снова вычислит результат для n=7 вместо использования предыдущей работы.


ваше решение будет работать (хотя и очень медленно), если у вас есть бесконечные уровни рекурсии. Но поскольку вы не повторно используете уже вычисленные результаты, вы очень быстро достигаете предела рекурсии Python. Одним из хороших решений, как было предложено выше, является переписывание кода с использованием нерекурсивного алгоритма.

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

  1. преобразовать весь код после первого else в функции next_move(n) это возвращает либо "Первый", либо "второй".

  2. добавить memoize(f) функция, которая будет принимать next_move(n) и избегайте вызова рекурсии, если результат для n уже рассчитан.

  3. добавить линию декоратора @memoize перед next_move определение.

результирующий код:

T = 8
ns = [1, 2, 3, 4, 5, 6, 7, 10]

legal_moves = [2, 3, 5]

def memoize(f):
    memo = {}
    def helper(x):
        if x not in memo:
            memo[x] = f(x)
        return memo[x]
    return helper

@memoize
def next_move(n):
    next_ns = map(lambda x: n - x, legal_moves)
    next_ns = filter(lambda x: x >= 0, next_ns)
    next_n_rewards = map(which_player_wins, next_ns)  # Reward for opponent
    if any(map(lambda x: x == "Second", next_n_rewards)):  # Opponent enters a losing position
        return "First"
    else:
        return "Second"

def which_player_wins(n):
    if n <= 1:
        return "Second"               # First player loses
    elif n in legal_moves:
        return "First"                # First player wins immediately
    else:
        return next_move(n)

for n in ns:
    print which_player_wins(n)

это чрезвычайно ускоряет вычисление, а также снижает уровни рекурсии, необходимые. На моем компьютере N=100 решается за 0.8 МС.


после Рори Долтонсовет использовать динамическое программирование, я переписал which_player_wins метод следующим образом:

# The lines below are for the Hackerrank submission
# T = int(raw_input().strip())
# ns = [int(raw_input().strip()) for _ in range(T)]

T = 8
ns = [1, 2, 3, 4, 5, 6, 7, 10]

def which_player_wins(n):
    moves = [2, 3, 5]
    table = {j:"" for j in range(n+1)}
    table[0] = "Second"     # If it is the first player's turn an no stones are on the table, the second player wins
    table[1] = "Second"     # No legal moves are available with only one stone left on the board

    for i in range(2,n+1):
        next_n = [i - move for move in moves if i - move >= 0]
        next_n_results = [table[k] for k in next_n]
        if any([result == "Second" for result in next_n_results]):
            table[i] = "First"
        else:
            table[i] = "Second"

    return table[n]

for n in ns:
    print which_player_wins(n)

это привело к успешному решению задачи (см. ниже).

enter image description here