LeetCode #70, JavaScript
Сегодня я начинаю изучать динамическое программирование и работать над задачей LeetCode Восхождение по лестнице.
Концепция проста, нам будет дана лестница из n ступенек. Мы можем делать один или два шага за раз, и нам нужно вернуть количество уникальных способов, которыми мы можем подняться по лестнице.
Итак, если бы у нас была лестница высотой всего в две ступени, мы могли бы подняться по ней двумя способами:
каждый шаг по отдельности или оба шага сразу. Наше возвращаемое значение здесь будет 2.
Кто знает, сколько подходов может сработать для этого. Я мог видеть, что это решаемо с использованием структуры перестановки или даже с некоторыми трудоемкими вложенными итеративными усилиями.
Но моя конечная цель всегда одна и та же с проблемами Leetcode — понять простейшую, самую элементарную природу того, что передо мной. И после нескольких тупиковых попыток я понял тенденцию увеличения числа решений от шага 0…
1 шаг = 1
2 шага = 2
3 шага = 3
4 шага = 5
5 шагов = 8…
Сюрприз!… это последовательность Фибоначчи
И это означает, что решение для каждого шага — это просто количество решений для двух предыдущих шагов. (шаг 3 имеет количество решений шага 1 + шага 2)
Итак, наши подзадачи — это решения для 1 шага и 2 шагов. Отсюда мы можем получить количество решений для любого последующего шага.
Так почему бы просто не реализовать функцию Фибоначчи и покончить с этим.
В конце концов, это просто и позволяет достичь того, что мы собираемся сделать. И технически это работает. Однако со всеми накладными расходами, связанными с рекурсией, вы, вероятно, получите тайм-аут от Leetcode.
В этом сценарии работает базовый итеративный подход:
Но одной из целей этой задачи является практика динамического программирования или запоминания.
Я слышал, что эти термины упоминаются почти взаимозаменяемо, но, работая над этой проблемой, я действительно хотел понять их различия.
Запоминание
Мемоизация — это метод, при котором мы сохраняем результаты предыдущих вычислений для использования в будущем.
Итак, давайте изменим рекурсивный подход, описанный выше, чтобы сделать это.
Итак, теперь мы будем добавлять в памятку о возврате каждого рекурсивного вызова.
Стоит отметить, что при решении задачи о подъеме по лестнице с помощью рекурсии, как я об этом думаю, мы на самом деле спускаемся с верхней ступеньки.
В строке 5, когда будет сделан первый рекурсивный вызов ClimbStairs, мы будем продолжать открывать новые контексты выполнения до тех пор, пока не вернем одно из наших значений из исходного мемо ([0, 1]). Затем эти контексты начнут закрываться и создавать нашу памятку.
С этого момента мы можем сократить значительное количество рекурсивных вызовов функций, обращаясь к этим ранее сохраненным значениям.
Динамическое программирование
Теперь, насколько я читал, динамическое программирование — это когда кто-то решает рекурсивную задачу итеративно, используя либо табуляцию, либо мемоизацию. Стремясь сделать это кратким, я не буду слишком глубоко погружаться в различия. Достаточно сказать, что мемоизация (как видно из приведенного выше примера) в конечном итоге использует нисходящий подход, работая сверху вниз по направлению к подзадачам.
С другой стороны, табулирование — это восходящий подход, при котором сначала решаются подзадачи, а затем они используются для построения окончательного решения.
Теперь мы начинаем с этого «нижнего шага» и попутно кэшируем вычисления, уменьшая рабочую нагрузку и приближаясь к нашему целевому шагу.
Скоростные пробные версии
Что быстрее — рекурсивное запоминание или подход динамического программирования с использованием табуляции?
Чтобы ответить на этот вопрос, я настроил несколько вызовов performance.now() для тактирования каждой функции с особенно значительными аргументами. Максимальное значение, которое я мог передать в рекурсивную функцию, было ~7000 без превышения максимального стека вызовов, так что это был мой ограничивающий фактор.
При таком размере входных данных динамический подход мог выполняться примерно в 5,6 раза быстрее, чем рекурсивная (запоминаемая) версия.
Этого более чем достаточно, чтобы убедить меня, что мне нужно посвятить значительно больше времени тому, чтобы сделать динамическое программирование знакомой техникой в моем наборе навыков.
Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Присоединяйтесь к нашему сообществу Discord.