Если вы спросите любого, кто готовится к собеседованию по кодированию, каковы одни из самых сложных проблем с кодированием, ответом, скорее всего, будут проблемы с динамическим программированием. Даже когда я готовился к собеседованию в 2014 году, я боялся динамического программирования и очень хотел, чтобы я никогда не встречал его на собеседовании. Я знал теорию, лежащую в основе решений некоторых классических задач динамического программирования, таких как задача о рюкзаке и самая длинная возрастающая подпоследовательность, но если меня спрашивали о проблеме, которую я никогда раньше не видел, мне требовалось несколько дней, чтобы найти решение. Так что я вообще избежал проблем с динамическим программированием. Но в прошлом году, когда я готовился к собеседованию, я хотел столкнуться с этими проблемами лицом к лицу, я хотел столкнуться со своими страхами и действительно потратить некоторое время, чтобы стать в этом хорошим. Поэтому я решил научиться решать любую проблему динамического программирования с нуля. Я начал гуглить о динамическом программировании и прочитал много статей, но большинство из них никогда не предлагали методик решения какой-либо проблемы с нуля, вместо этого большинство из них объясняли, что такое динамическое программирование, и объясняли некоторые из наиболее распространенных проблем динамического программирования. А некоторые статьи носили слишком математический характер и больше подходят для академиков, а не для интервью. Я начал решать проблемы с Hackerrank и leetcode специально помечал проблемы динамического программирования. Многие из вас могут посочувствовать мне, что решение проблем алгоритмов с нуля без особой помощи расстраивает, и когда вы сталкиваетесь с препятствием, это также очень деморализует. Вы просто хотите прекратить это делать и пообещать себе, что, закончив собеседование, вы больше никогда не коснетесь этих проблем с алгоритмом.

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

Возврат

Небольшая предыстория, связанная с этим. Еще в 2014 году, когда я работал в Samsung, ни с того ни с сего ко мне обратился рекрутер из Google по поводу должности инженера-программиста. Я прошел телефонное собеседование без особой подготовки, так как серьезно не искал смены работы (но эй, это Google). К моему полному удивлению, они хотели, чтобы я приехал на собеседование. Я никогда в своих мечтах не думал, что когда-нибудь пройду интервью в Google, не говоря уже о том, чтобы работать на них. Я действительно хотел хорошо подготовиться и хорошенько постараться. Я искал в Интернете советы по подготовке к интервью в Google, что читать, какие раунды будут там проходить, и наткнулся на эту удивительную книгу Стива С. Скиенны под названием Руководство по разработке алгоритмов. До этого времени моей подготовки к собеседованию было недостаточно для этих крупных компаний, особенно циклов программирования (вот почему я работал в Samsung). Я сконцентрировался в основном на основах информатики и решил несколько задач на собеседовании Взломать код. Я думал, что этого достаточно (вздыхает). Эта книга открыла мне глаза на мир разработки алгоритмов, о существовании которого я даже не подозревал. Он познакомил меня с некоторыми методами разработки алгоритмов, такими как поиск с возвратом, поиск в глубину, поиск в ширину, и помог мне понять, как применять эти методы. Я не смог пройти собеседование в Google, и мне пришлось некоторое время оставаться в Samsung, но то, что я узнал из этой книги, осталось со мной, и это заставило меня по-другому взглянуть на эти проблемы алгоритмов. Одной из техник, которые меня увлекли, были рекурсия и отслеживание с возвратом. В этой книге автор дал общую основу для решения задач исчерпывающего поиска. Я был удивлен тем, сколько проблем, которые задают на собеседовании, можно решить с помощью этой техники. Я попытался применить обратное отслеживание почти ко всем проблемам, по крайней мере, это помогло мне получить наивное решение проблем, например, перестановки, комбинации, поиск в глубину и т. Д.

Мышление сверху вниз

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

Воспоминание

Я всегда думал, что динамическое программирование заполняет таблицы результатами снизу вверх и использует их для вычисления других результатов, потому что это то, что я читал в учебниках как во время учебы в бакалавриате, так и во время учебы в магистратуре. Я слышал об этом слове «мемоизация», но мне никогда не хотелось понимать, что именно оно означает. Итак, пока я решал задачи DP, у меня появилась возможность пересмотреть эту концепцию. Вскоре я понял, что Мемоизация - это не что иное, как вариант динамического программирования. В мемоизации все, что вы делаете, - это рекурсивная реализация решения и кэширование результатов в структуре данных, обычно в массиве или хэш-карте, используя значения входных параметров в качестве ключа. Если результат присутствует, мы вернем это кешированное значение или воспользуемся рекурсией для вычисления результатов. Это было ключевым осознанием, изменившим мой взгляд на проблемы DP. Я смог использовать рекурсию, отслеживание с возвратом и мемоизацию для решения почти всех проблем DP. В Leetcode и Hackerrank хорошо то, что они позволяют сравнивать время выполнения вашего решения с другими решениями на том же языке программирования, и я был удивлен, увидев, что мои решения с использованием мемоизации работают хорошо, они не были самыми быстрыми решениями, но я могу говорят, что они были лучше, чем 70 процентов решений. Я начал получать удовольствие от всего процесса использования рекурсии для разработки решений и использования кеша для хранения и получения результатов. Для кеша я изначально использовал HashMap, и если было более одного параметра, я использовал для создания строки, разделенной запятыми, с использованием значений параметров и использовал это как ключ к кешу. Но я понял, что использование массива для кеширования намного быстрее, даже несмотря на то, что для этого требуется больше памяти. 1D массив, если есть один параметр или 2D массив, если есть 2 параметра и так далее.

Восходящее решение

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

  1. Инициализировать массив. Размер массива зависит от количества параметров, определяющих состояние.
  2. Для каждого параметра используйте цикл for, чтобы перебрать все возможные значения параметра.
  3. В рекурсивном решении, если значение параметра уменьшается при рекурсивном вызове функции, мы перебираем значение параметра в порядке возрастания. Если значение увеличивается в рекурсивном вызове, мы перебираем значение в порядке убывания.
  4. Внутри цикла for примените рекурсивную формулу, но вместо вызова рекурсивной функции используйте результаты из кеша.

Восходящее решение работает намного лучше, чем рекурсивное решение, поскольку рекурсия вызывает много накладных расходов. Я смог протестировать этот пошаговый подход для решения новых проблем DP с нуля, и он отлично сработал. Я смог решить новые задачи менее чем за час. Я собрал много разных задач DP из разных источников, чтобы протестировать этот подход, некоторые из них - leetcode, codechef, hackerrank и geek for geeks. Подход на удивление хорошо работал с эффективными решениями.

Овладейте искусством динамического программирования

Я подумал о том, чтобы поместить то, что я изучил до сих пор, в курс, чтобы он мог помочь моим друзьям и всем, кто может извлечь выгоду из этого подхода, с легкостью попрактиковаться в решении все большего и большего числа задач динамического программирования. Вот почему я создал этот курс по Udemy под названием «Овладейте искусством динамического программирования».



(Вы можете перейти по этой ссылке, чтобы купить курс со скидкой 50%).

В нем 4,5 часа курсовых видео. Обсуждаемые темы:

  1. Рекурсия
  2. Возврат
  3. Введение в динамическое программирование
  4. Проблемы оптимизации
  5. Пошаговый подход к решению задач DP
  6. Одномерное динамическое программирование
  7. Двумерное динамическое программирование

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