Использование программирования Dynami c для решения варианта задачи о рюкзаке - PullRequest
2 голосов
/ 06 мая 2020

Я работаю через MIT6.0002 на OpenCourseWare (https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0002-introduction-to-computational-thinking-and-data-science-fall-2016/assignments/), и я озадачен частью B набора задач 1. Проблема, которая представлена ​​как версия задачи о рюкзаке, формулируется следующим образом:

[Ауки обнаружили колонию гусей, откладывающих золотые яйца разного веса] Они хотят нести как можно меньше яиц в своем путешествии, так как у них нет много места на своих кораблях. Они сделали подробные записи о весе всех яиц, которые гуси могут отложить в данной стае, и о том, какой вес могут выдержать их корабли. Реализуйте алгоритм программирования Dynami c, чтобы найти минимальное количество яиц, необходимое для получения заданного веса для определенного корабля в dp_make_weight. Результатом должно быть целое число, представляющее минимальное количество яиц от данной стаи гусей, необходимое для получения заданного веса. Вашему алгоритму не требуется возвращать вес яиц, а только минимальное количество яиц. Допущения: -Все яйца по весу у разных гусей уникальны, но заданное goose всегда откладывает яйца одинакового размера - Ауки могут ждать, пока гуси отложат столько яиц, сколько им нужно [ie есть бесконечное количество яиц каждого размера]. -Всегда доступны яйца размера 1

Проблема также утверждает, что решение должно использовать динамическое c программирование. Я написал решение (в Python), которое, как мне кажется, находит оптимальное решение, но оно не использует программирование на динамике c, и я не понимаю, как применимо программирование на динамике c. Также было предложено, чтобы решение использовало рекурсию.

Может ли кто-нибудь объяснить мне, в чем преимущество использования мемоизации в этом случае и что я получу от реализации рекурсивного решения? (Приносим извинения, если мой вопрос слишком расплывчатый или решение слишком очевидно для слов; я относительный новичок в программировании и на этом сайте).

Мой код:

#================================
# Part B: Golden Eggs
#================================

# Problem 1
def dp_make_weight(egg_weights, target_weight, memo = {}):
    """
    Find number of eggs to bring back, using the smallest number of eggs. Assumes there is
    an infinite supply of eggs of each weight, and there is always a egg of value 1.

    Parameters:
    egg_weights - tuple of integers, available egg weights sorted from smallest to largest value (1 = d1 < d2 < ... < dk)
    target_weight - int, amount of weight we want to find eggs to fit
    memo - dictionary, OPTIONAL parameter for memoization (you may not need to use this parameter depending on your implementation)

    Returns: int, smallest number of eggs needed to make target weight
    """
    egg_weights = sorted(egg_weights, reverse=True) 
    eggs = 0
    while target_weight != 0:
        while egg_weights[0] <= target_weight:
            target_weight -= egg_weights[0]
            eggs += 1
        del egg_weights[0]
    return eggs


# EXAMPLE TESTING CODE, feel free to add more if you'd like
if __name__ == '__main__':
    egg_weights = (1, 5, 10, 25)
    n = 99
    print("Egg weights = (1, 5, 10, 25)")
    print("n = 99")
    print("Expected ouput: 9 (3 * 25 + 2 * 10 + 4 * 1 = 99)")
    print("Actual output:", dp_make_weight(egg_weights, n))
    print()

1 Ответ

2 голосов
/ 07 мая 2020

Проблема здесь - классическая ситуация c DP, когда жадность может иногда давать оптимальные решения, а иногда нет.

Ситуация в этой задаче аналогична классической c проблеме DP размену монет где мы wi sh, чтобы найти наименьшее количество монет разного достоинства для внесения сдачи с заданной целью. стоимость. Номиналы, доступные в некоторых странах, таких как США (в которых используются монеты достоинством 1, 5, 10, 25, 50, 100), таковы, что оптимально жадно выбирать самую большую монету, пока ее стоимость не упадет ниже ее, а затем перейти к следующая монета. Но с другими наборами номиналов, такими как 1, 3, 4, жадный выбор самого большого значения многократно может привести к неоптимальным результатам.

Точно так же ваше решение отлично работает для одного веса яиц, но не работает для других. Если мы выберем для наших яиц веса 1, 6, 9 и дадим целевой вес 14, алгоритм сразу же выберет 9, а затем не сможет добиться прогресса на 6. В этот момент он проглотит кучу единиц и в конечном итоге думает 6 - минимальное решение. Но это явно неверно: если мы разумно проигнорируем 9 и выберем сначала две шестерки, то сможем достичь желаемого веса всего с 4 яйцами.

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

def dp_make_weight(egg_weights, target_weight):
    least_taken = float("inf")

    if target_weight == 0:
        return 0
    elif target_weight > 0:
        for weight in egg_weights:
            sub_result = dp_make_weight(egg_weights, target_weight - weight)
            least_taken = min(least_taken, sub_result)

    return least_taken + 1

if __name__ == "__main__":
    print(dp_make_weight((1, 6, 9), 14))

Для каждого вызова у нас есть 3 возможности:

  1. Базовый случай target_weight < 0: вернуть что-то, чтобы указать решение невозможно (для удобства я использовал бесконечность).
  2. Базовый случай target_weight == 0: мы нашли возможное решение. Верните 0, чтобы указать, что здесь не было предпринято никаких шагов, и дайте вызывающей стороне базовое значение для увеличения.
  3. Рекурсивный случай target_weight > 0: попробуйте взять каждый доступный egg_weight, вычтя его из общего числа и рекурсивно исследуя корневой путь в новом состоянии. Изучив все возможные исходы текущего состояния, выберите тот, который сделал наименьшее количество шагов для достижения цели. Добавьте 1, чтобы подсчитать пройденное яйцо на текущем шаге и вернуться.

До сих пор мы видели, что жадное решение неверно и как его исправить, но не мотивировало динамическое c программирование или мемоизация. DP и мемоизация - это чисто оптимизационные концепции, поэтому вы можете добавить их после того, как найдете правильное решение и вам потребуется его ускорить. Сложность приведенного выше решения по времени экспоненциальна: для каждого вызова мы должны вызывать len(egg_weights) рекурсивные вызовы.

Есть много ресурсов, объясняющих DP и мемоизацию , и я уверен, что ваш курс покрывает это, но вкратце, наше рекурсивное решение, показанное выше, повторно вычисляет одни и те же результаты снова и снова, выбирая разные рекурсивные пути, которые в конечном итоге приводят к тем же значениям, которые даются для target_weight. Если мы храним памятку (словарь), в которой результаты каждого вызова хранятся в памяти, то всякий раз, когда мы снова сталкиваемся с вызовом, мы можем искать его результат вместо того, чтобы заново вычислять его с нуля.

def dp_make_weight(egg_weights, target_weight, memo={}):
    least_taken = float("inf")

    if target_weight == 0:
        return 0
    elif target_weight in memo:
        return memo[target_weight]
    elif target_weight > 0:
        for weight in egg_weights:
            sub_result = dp_make_weight(egg_weights, target_weight - weight)
            least_taken = min(least_taken, sub_result)

    memo[target_weight] = least_taken + 1
    return least_taken + 1

if __name__ == "__main__":
    print(dp_make_weight((1, 6, 9, 12, 13, 15), 724)) # => 49

Поскольку мы используем Python, способ "Pythoni c" сделать это, вероятно, заключается в украшении функции. Фактически, есть встроенный мемоизатор под названием lru_cache, поэтому, возвращаясь к нашей исходной функции без какой-либо мемоизации, мы можем добавить мемоизацию (кеширование) с помощью двух строк кода:

from functools import lru_cache

@lru_cache
def dp_make_weight(egg_weights, target_weight):
    # ... same code as the top example ...

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

...