<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <language>ru</language>
    <title>Рекурсия и динамика</title>
    <description/>
    <link>https://tproger.ru/tag/recursion-and-dynamic-programming</link>
    <atom:link href="https://tproger.ru/tag/recursion-and-dynamic-programming/feed" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Sun, 04 Oct 2026 11:08:00 GMT</lastBuildDate>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>12</sy:updateFrequency>
    <image>
      <title>Рекурсия и динамика</title>
      <link>https://tproger.ru</link>
      <url>https://tproger.ru/apple-touch-icon.png</url>
    </image>
    <item>
      <title>Что такое рекурсия и как с ней работать: разбираемся на примерах</title>
      <link>https://tproger.ru/articles/chto-takoe-rekursiya-i-kak-s-nej-rabotat</link>
      <comments>https://tproger.ru/articles/chto-takoe-rekursiya-i-kak-s-nej-rabotat?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Анна Ельцова]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/chto-takoe-rekursiya-i-kak-s-nej-rabotat</guid>
      <description><![CDATA[<p>Что такое рекурсия - Основы работы с рекурсией - Tproger
description: Что такое рекурсия. Показываем, как правильно работать с рекурсией и когда она нужна. Рассматриваем пошаговую инструкцию и примеры ✔ Tproger
</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/chto-takoe-rekursiya-i-kak-s-nej-rabotat">Что такое рекурсия и как с ней работать: разбираемся на примерах</a>»</p>]]></description>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Языки программирования]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[Haskell]]></category>
      <category><![CDATA[Scala]]></category>
      <category><![CDATA[Графы]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 05 Mar 2025 14:00:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>Рекурсия — это мощный инструмент в программировании, который позволяет решать задачи, разбивая их на более простые подзадачи. В статье рассмотрим базовые понятия рекурсии, её принципы, примеры использования, а также типичные проблемы, с которыми можно столкнуться при написании кода.</p><h2>Рекурсия: что это такое и зачем нужна</h2><p>Рекурсия — функция, которая вызывает саму себя. Ее базовое применение — разбить большую задачу на несколько мелких, что делает код проще и понятнее, или когда нужно повторить какое-то действие несколько раз. Второй случай похож на русскую матрешку — вы достаете куклу за куклой, пока не дойдете до самой маленькой.</p><figure><img src="https://media.tproger.ru/user-uploads/101078/2025-02-27/1ac7a0f3-4cad-4616-8226-f5814dbc266e.jpg" alt="" /></figure><p>А теперь давайте разберемся на простом примере из математики: представим, что у нас есть функция F, которая выполняет определенное действие. В нашем случае — вычисляет факториал числа (это самый классический пример). Внутри этой функции F в качестве одного из значений используем вызов этой же функции F, но с уменьшенным аргументом. Так функция будет вызывать саму себя до тех пор, пока не достигнет базового случая (о нем расскажем ниже), после чего начнет возвращать результаты.</p><p>В виде кода это выглядит так:</p><p>Здесь функция F вычисляет факториал числа n, умножая его на факториал n-1, и так до тех пор, пока не дойдёт до 1, после чего начнёт возвращать результаты вверх по цепочке вызовов. Этот пример как раз о том, что нужно выполнить несколько одинаковых действий.</p><p>А вот пример из жизни: вы листаете ленту в известной социальной сети, а новые посты обновляются автоматически — это рекурсия в действии.</p><h2>Принципы работы рекурсии</h2><p>Рекурсия строится на двух ключевых принципах: базовый случай и рекурсивный вызов. Они позволяют функции вызывать саму себя, пока она не завершится.</p><h3>Базовый случай</h3><p>Базовый случай рекурсии — условие, при котором рекурсивный вызов прекращается. Без него функция будет вызывать саму себя бесконечно, а значит, стек переполнится.</p><p>Один из самых простых примеров — факториал, о котором мы писали выше. Другой — возведение числа в степень до тех пор, пока значение не станет равно искомому.</p><p>Так это выглядит в виде кода:</p><p>То есть, базовый случай — если exponent == 0, то возвращаем 1.</p><h3>Рекурсивный вызов</h3><p>Рекурсивный вызов — шаг, на котором функция вызывает саму себя, но с измененными параметрами, чтобы задача стала проще. Один из еще одних классических примеров — задача с числами Фибоначчи.</p><p>Числа Фибоначчи — это последовательность, где каждое число равно сумме двух предыдущих: 0, 1, 1, 2, 3, 5, 8, 13, 21, …</p><p>Если вызвать fibonacci(5), то увидите следующее:</p><p>После этого функция возвращает результаты обратно вверх по стеку вызовов. Обычно этот метод используется в криптографии и алгосах.</p><p>Кстати, на примере чисел Фибоначчи можно разобрать одну из проблем рекурсии — плохая производительность при отсутствии мемоизации.</p><p>Мемоизация — техника оптимизации, при которой результаты больших вычислений сохраняются и используются еще раз при повторных вызовах функции. Это особенно полезно, если работаете с рекурсиями, которые могут вычислять одни и те же значения много раз.</p><p>Так, функция  в задаче с числами Фибоначчи имеет экспоненциальную сложность O(2^n), так как она многократно вычисляет одни и те же значения. Например, чтобы вычислить fibonacci(5), функция вызывает сама себя 15 раз, хотя уникальных вычислений всего 6.</p><p>Чтобы улучшить производительность, нужно использовать мемоизацию, чтобы сохранить результаты:</p><p>Здесь сложность уменьшается до O(n), так как каждое значение вычисляется только один раз.</p><h2>Стек вызовов и управление памятью</h2><p>Каждый рекурсивный вызов функции добавляет новый элемент в стек вызовов. Это значит, что вызовы складывается в стеке до тех пор, пока не будет достигнут базовый случай. После этого стек начинает разворачиваться, возвращая результаты на каждом уровне.</p><p>Например:</p><p>Так, когда мы вызываем сountdows(3), в стеке появляются новые вызовы:</p><p>call countdown(3)  # → печатает 3</p><p>call countdown(2)  # → печатает 2</p><p>call countdown(1)  # → печатает 1</p><p>call countdown(0)  # → печатает "Старт" и выходит</p><p>Затем стек начинает разворачиваться:</p><ul><li>countdown(0) завершился → удаляется из стека</li><li>countdown(1) завершился → удаляется из стека</li><li>countdown(2) завершился → удаляется из стека</li><li>countdown(3) завершился → удаляется из стека</li></ul><p>Если же в рекурсии нет базового случая, то стек вызовов никогда не освободится, и программа (в нашем случае на Питоне) выдаст ошибку RecursionError.</p><p>Вот пример бесконечной рекурсии:</p><p>В Питоне есть ограничения до 1000 вызовов, чтобы не перегружать стек, но этот лимит можно увеличить:</p><h2>Рекурсия на реальных задачах</h2><p>С базовым случаем и рекурсивным вызовом все понятно, поэтому давайте разбираться на более реальных и сложных примерах — с деревьями, графами и массивами.</p><h3>Обход структуры данных</h3><p>Рекурсия часто используется для обхода деревьев и графов. Например, бинарного дерева — в нем не более двух дочерних узлов (левый и правый). Вот три основных способа обхода:</p><ol><li>Прямой обход (Pre-order): Корень → Левый узел → Правый узел.</li><li>Симметричный обход (In-order): Левый узел → Корень → Правый узел.</li><li>Обратный обход (Post-order): Левый узел → Правый узел → Корень.</li></ol><p>Запишем в виде кода:</p><p>Дерево будет выглядеть так:</p><p>1</p><p>/ \</p><p>2   3</p><p>/ \</p><p>4   5</p><ul><li>Прямой обход: 1 → 2 → 4 → 5 → 3.</li><li>Симметричный обход: 4 → 2 → 5 → 1 → 3.</li><li>Обратный обход: 4 → 5 → 2 → 3 → 1.</li></ul><p>Обходить можно и графы, особенно в глубине (DFS). Вот пример:</p><p>Сам граф:</p><p>Результат обхода: A → B → D → E → F → C.</p><h3>Разбиение</h3><p>Разбить можно массив или строку. Давайте разбираться на примере строки. Так, рекурсия может разбить строку на слова или разделить ее по конкретным символам. Выглядит это так:</p><h2>Что такое хвостовая рекурсия</h2><p>Хвостовая (оконечная) рекурсия — частный случай рекурсии. Главное отличие от обычной в том, что единственный рекурсивный вызов находится в конце перед выходом функции. В традиционной же рекурсии после рекурсивного вызова могут быть дополнительные вычисления.</p><p>Вот пример:</p><p>Кстати, с помощью хвостовой рекурсии тоже можно решать задачи с числами Фибоначчи:</p><p>Некоторые языки программирования (например, Haskell и Scala) поддерживают оптимизацию хвостовой рекурсии. Это означает, что компилятор или интерпретатор преобразует хвостовую рекурсию в итерацию — это помогает избежать переполнение стека.</p><p>Давайте посмотрим, как это работает в Scala. Вот обычная рекурсия, чтобы понять принцип:</p><p>А вот рекурсия с оптимизацией:</p><p>К сожалению, наш любимый Питон такую функцию не поддерживает, но всегда можно преобразовать «хвостика» в итерацию вручную:</p><p>Это пример итеративного решения факториала.</p><h2>Рекурсия vs. итерация</h2><p>Основные различия между рекурсией и итерацией заключаются в следующем:</p><ul><li>В рекурсии функция вызывает саму себя, а в итерации используются циклы.</li><li>Интеграция требует меньше памяти, потому что нет стека вызовов.</li><li>Рекурсия медленнее без оптимизации.</li><li>С рекурсией легко переполнить хранилище при большой глубине.</li><li>Рекурсия обычно более читаемая и лаконичная, но подходит для меньшего количества задач.</li></ul><p>Итерацию лучше использовать в следующих случаях:</p><ul><li>Задачи, где нужна производительность. Как, например, с итеративным вычислением чисел Фибоначчи.</li><li>Обработка больших данных, например, поиск или сортировка. Вот пример пузырьковой сортировки:</li></ul><ul><li>Задачи с большой глубиной рекурсии, чтобы стек не переполнялся. Вот как итеративно обойти дерево:</li></ul><h2>Еще один пример: задача «Ханойские башни»</h2><p>«Ханойские башни» — классная головоломка, которую часто решают с помощью рекурсии. Считайте, что это еще одна классика.</p><p>Есть три стержня: A, B и C. На стержень A нанизано несколько дисков разного размера, причем диски упорядочены по размеру в виде пирамидки — вниз от маленького к большому. Суть в том, чтобы переместить все диски со стержня А на С, при этом: за один раз можно перемещать только один диск, нельзя класть больший диск на меньший, можно использовать стержень B как вспомогательный.</p><figure><img src="https://media.tproger.ru/user-uploads/101078/2025-02-27/c2ccaa24-e4b7-4d2f-8ae8-ac894801834a.png" alt="" /></figure><p>Рекурсивное решение задачи «Ханойские башни» основано на следующей идее:</p><ol><li>Переместить (n−1) дисков с A на B, используя C как промежуточный.</li><li>Переместить оставшийся самый большой диск с A на C.</li><li>Переместить (n−1) дисков с B) на C, теперь используя A как промежуточный.</li></ol><p>Следовательно, если количество дисков n=1, просто переместите его с А на С (базовый случай). В остальных случаях при n–1 повторить то, что указано выше.</p><p>А теперь решение с помощью кода:</p><p>Вывод:</p><p>Поздравляем, вы великолепны. А если хотите еще потренироваться в рекурсии, то можно написать код для поиска пути в лабиринте (кстати, это чем-то напоминает онлайн-карты и навигаторы) или обратного вывода строки.</p>]]></content:encoded>
    </item>
    <item>
      <title>Идеи динамического программирования: одномерные задачи, часть 2</title>
      <link>https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-2</link>
      <comments>https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-2?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Maksim Petrikov]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-2</guid>
      <description><![CDATA[<p>Основные задачи динамического программирования, которые можно решить, используя одномерный массив.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-2">Идеи динамического программирования: одномерные задачи, часть 2</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[JavaScript]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Пост пользователя]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Mon, 27 Sep 2021 08:47:24 GMT</pubDate>
      <content:encoded><![CDATA[<p>В первой части статьи мы разобрали несколько типичных задач динамического программирования с использованием одномерного массива для хранения подзадач.</p><p>В этой статье продолжим рассмотрение подобных задач.</p><p>Вернемся к кузнечику, который прыгал по кувшинкам в первой статье.</p><h2>Кузнечик, кувшинки и монетки</h2><p>В этой задаче на кувшинки добавляются монетки, причём их можно как собирать, так и терять. Теперь нам неважно, каким путем двигается кузнечик, главное — собрать максимальное количество монеток к последней кувшинке.</p><p>Немного изменим условия прыжков. Теперь кузнечик может прыгать на 1, 2 или 3 кувшинки вперёд. Также кузнечик не может терять больше монеток, чем у него есть в наличии.</p><p>Как мы уже знаем, две самые главные вещи в динамическом программировании — начальные значения и переход от одной подзадачи к другой. Давайте посмотрим, что у нас является подзадачей, т.е. какие значения будем хранить.</p><p>Для каждой кувшинки мы будем хранить максимальное количество монет, которое можно собрать, добравшись до неё.</p><p>Рассмотрим пятую кувшинку, на которой мы теряем 3 монетки. Откуда мы можем прыгнуть на неё? Со второй, третьей или четвёртой кувшинки. Откуда мы должны прыгнуть на неё, чтобы заработать максимальное количество монеток? С кувшинки, на которой наше заработанное количество монеток было максимальным. Это будет выглядеть примерно так:</p><p>Подумаем над начальным значением. Мы можем через несколько if’ов посчитать, сколько будет на каждой из кувшинок заработано монеток. Но если в данном случае это будет довольно просто, то, например, если кузнечик сможет прыгать на 10 кувшинок, то это приведёт к очень сложному и запутанному коду. Попробуем обойтись без начальных значений, а предыдущие значения, если они выходят за границы массива, игнорировать.</p><p>В примере с распределение монеток [4, -2, 1, -1, -3] максимальная сумма будет 2. Кузнечик прыгнет на первую, третью и пятую кувшинки. На пятую прыгать обязательно, т.к. это условие задачи — достигнуть последнюю кувшинку.</p><h3>Путь, чтобы собрать максимум монеток</h3><p>Дополним задачу — теперь нам нужно не просто найти, сколько максимально можно собрать монет, добравшись до последней кувшинки, а ещё и найти путь, как это было сделано.</p><p>Часто в задачах динамического программирования применяется следующая техника: берём наше вычисленное значение, идём от конца к началу массива с описанием максимума на каждой кувшинке (в данном случае maxCoins), восстанавливая обратную последовательность вычислений: будем находить максимальное значение в предыдущих ячейках массива maxCoins.</p><p>Также мы можем найти путь другим способом — ввести дополнительную структуру данных и хранить в ней путь, по которому мы добрались до текущей ячейки. Код выглядит немного проще, но при работе с большим объемом данных хранение путей может занимать значительное место в памяти. Посмотрим на решение:</p><p><a href="https://leetcode.com/problems/min-cost-climbing-stairs/">Задача для практики на LeetCode</a></p><h2>Наибольшая (по длине) возрастающая подпоследовательность</h2><p>Подпоследовательность — это любая последовательность внутри массива, расположенная по возрастанию индексов. Например? в массиве [1, 2, 3, 4, 5] набор чисел [1, 3, 5] является подпоследовательностью. [1, 4, 2] не является, т.к. 2 расположена перед 4.</p><p>Задача — написать функцию, которая бы возвращала максимальную подпоследовательность.</p><p>Рассмотрим массив [1, 5, 3, 6, 4, 5, 2]. Несколько его возрастающих подпоследовательностей: [1, 5, 6], [1, 5], [1, 3], [1, 3, 5], [1, 3, 4, 5]. Наибольшая из всех таких подпоследовательностей размера 4:</p><figure><img src="https://media.tproger.ru/uploads/2021/09/Screenshot-2021-09-21-at-13.32.00.png" alt="" /></figure><p>Для начала посмотрим, как можно решить задачу про максимальную длину такой подпоследовательности. Введём массив, который будет хранить максимальную длину для текущего элемента. Например, для последовательности выше [1, 5, 3, 6, 4, 5, 2] такой массив в конце вычислений будет содержать значения [1, 2, 2, 3, 3, 4, 2]. Назовем его, например, longestSubsequence. Для первого элемента начальное значение очевидно — последовательность, состоящая только из этого элемента. Добавим начальное значение:</p><p>Для каждого последующего элемента будем искать в исходном массиве значение меньше текущего, проверять, последовательность какой длины можно составить с добавлением текущего элемента, и выбирать максимальную.</p><p>Например, мы добрались до элемента 4, longestSubsequence будет содержать в себе [1, 2, 2, 3]. Ищем в исходном массиве элементы, меньше чем 4, это 1 и 3. Длина подпоследовательностей для этих элементов: 1 и 2. Если добавить после 1 число 4, то получим длину последовательности 2: longestSubsequence[0] + 1, если после 3, то длина будет 3: longestSubsequence[2] + 1. Добавляем единицу к longestSubsequence элементу, т.к. добавление нового элемента увеличивает подпоследовательность.</p><p>Выяснили начальные значение и как переходить от одной подзадачи к другой, остался вопрос, где брать ответ. Максимальная возрастающая подпоследовательность может закончиться в любом месте исходного массива. Значит, нужно просто найти максимальное значение в массиве longestSubsequence. Посмотрим на решение подзадачи про нахождение длины:</p><p>Доработаем задачу, чтобы можно было находить не только длину, но и саму подпоследовательность. Для этого есть два пути. Можем хранить в массиве максимальную подпоследовательность в текущей позиции целиком или просто индекс предыдущего элемента для позиции.</p><p>Хранение подпоследовательностей может привести к значительному расходу памяти для больших значений. Рассмотрим решение с хранением индекса предыдущего элемента:</p><p><a href="https://leetcode.com/problems/longest-increasing-subsequence/">Задача для практики на LeetCode</a></p><h2>Игра — делители</h2><p>Рассмотрим <a href="https://leetcode.com/problems/divisor-game/">ещё одну задачу с сайта Leetcode</a>. Если не быть знакомыми с методами динамического программирования и не понять, что их здесь стоит применять, то решение этой задачи может быть очень сложным.</p><p>Есть два игрока, в задаче — Алиса (или Элис) и Боб. Дано число, например, 5. Игроки по очереди уменьшают число следующим действием: находят целочисленный делитель текущего числа (не само число), вычитают его из текущего числа. Так получается следующее число. Первой ходит Алиса.</p><p>Цель игры — определить, сможет ли выиграть Алиса при идеальной игре обоих участников. Вернуть true, если сможет.</p><p>Рассмотрим случай с числом 5. Ход Алисы. Число 5 имеет 2 делителя: 1 и 5. Но выбрать 5 по условию задачи мы не можем, выбираем единицу. Вычитаем её, получаем число 4 для следующего хода.</p><p>Ход Боба. 4 имеет три делителя, мы можем использовать только 1 или 2. Допустим, что используем 1, для Алисы оставляем число 3.</p><p>Дальнейшая игра очевидна и не предоставляет выбор для игроков.</p><p>Возникает проблема: какое число оптимально использовать, когда есть несколько вариантов делителей? Например, в рассматриваемом случае Боб на втором шаге мог выбрать делитель 1 или 2. Мы могли построить дерево до конца игры, и если у Алисы нет выбора, и в одном из вариантов выиграет Боб, то тот ход был оптимальным. Но что если у Алисы есть выбор, как определить оптимальность?</p><p>Рассмотрим игру для числа 6:</p><p>У Алисы на первом ходу есть на выбор три числа для вычитания: 1, 2, 3 — целочисленные делители числа 6. У Боба тоже появляется ветвление, если Алиса выбрала число 2 и оставила для Боба число 4. Когда мы дойдём до конца этого дерева, то как определить, были ли все ходы оптимальны? Таким подходом, сверху вниз, эту задачу не решить.</p><p>Рассмотрим другой подход, снизу вверх. Будем идти от единицы и выше, постараемся определить, кто выиграл на каждом шаге. Для хранения результатов создадим массив, хранящий возможность выигрыша для того, чей ход текущий.</p><p>Например, если у Алисы число 2, то берем первый элемент из этого массива. Он true, значит, Алиса может выиграть. Для числа 5 — false, значит, если оба игрока играют оптимальным способом, то Алиса проиграет. Как же создать такой массив?</p><p>Для числа 1 ответ очевиден: Алиса не сможет вычесть из этого числа само себя и сразу проигрывает.</p><p>Число 2. Может ли Алиса сделать так, что Боб попадет в ячейку со значением false, т.е. гарантированно проиграет? Да, Алиса может вычесть единицу, для Боба останется число 1, которое проигрывает для того, чей ход. Мы знаем, что число 1 проигрывает для того, кто ходит, т.к. именно эта информация хранится в массиве gameResultForStartPlayer.</p><p>Рассмотрим, например, число 9. Массив gameResultForStartPlayer уже должен быть заполнен для всех предыдущих чисел. Сможет ли Алиса сделать такой ход, чтобы Боб проиграл? Целочисленные делители числа 9 — 1 и 3. Бобу достанется либо 8, либо 6. У обоих этих чисел значение в массиве — true, следовательно, при оптимальной игре Боб выиграет. Соответственно, если Алисе достанется число 9, то она проиграет, и при заполнении массива следует в него добавить значение false.</p><p>Рассмотрим решение:</p><h2>Заключение</h2><p>В двух статьях мы рассмотрели основные задачи динамического программирования, которые можно решить, используя одномерный массив. Это наиболее простые примеры этого способа решения задач, разбираясь с которыми, можно понять основные концепции динамического программирования и двигаться дальше.</p>]]></content:encoded>
    </item>
    <item>
      <title>Идеи динамического программирования: одномерные задачи, часть 1</title>
      <link>https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-1</link>
      <comments>https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-1?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Maksim Petrikov]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-1</guid>
      <description><![CDATA[<p>Разбираем несколько классических задач динамического программирования с использованием одномерного массива и без него.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/idei-dinamicheskogo-programmirovanija-odnomernye-zadachi-chast-1">Идеи динамического программирования: одномерные задачи, часть 1</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[JavaScript]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Пост пользователя]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 22 Sep 2021 06:22:42 GMT</pubDate>
      <content:encoded><![CDATA[<p>Динамическое программирование — способ решения задач путём разбиения их на подзадачи, которые, в свою очередь, используются для нахождения ответа исходной задачи. В этой статье мы разберём несколько классических задач динамического программирования, которые требуют для своего решения одномерный массив, либо, в некоторых случаях, можно даже обойтись без него.</p><p>Если вы не знакомы с динамическим программированием, посмотрите эту <a href="https://tproger.ru/articles/dynprog-starters/">статью для начинающих</a>.</p><h2>Числа трибоначчи</h2><p>Числа трибоначчи — это числовой ряд, начинающийся с трёх чисел 0, 0, 1, где каждое последующее число вычисляется как сумма трёх чисел перед ним:</p><figure><img src="https://media.tproger.ru/uploads/2021/08/Screenshot-2021-08-28-at-03.11.30.png" alt="" /></figure><p>Рассмотрим следующую задачу: дан массив чисел с порядковыми номерами чисел трибоначчи, например: [73, 10, 4, 15, 20, 7], числа в массиве не превышают 100, требуется вернуть массив, содержащий числа трибоначчи под указанными номерами: [73-е число трибоначчи, 10-е число трибоначчи, и так далее].</p><p>Если вы какое-то время работаете с языками программирования, то наверняка решали задачи нахождения факториала или степени числа через рекурсию. Попробуем применить такую же идею здесь:</p><p>Запускаем функцию с вычислением времени:</p><p>41-е число вычисляется 135 секунд. Кажется, что быстрее вручную посчитать числа, подставить в массив все 100 значений (ограничение задачи) и просто вернуть число на нужной позиции. В чём же проблема, как так получилось, что простой алгоритм работает так долго? Можно заметить, что следующее число вычисляется в 2 раза дольше, чем предыдущее. Давайте посмотрим на рекурсивное дерево вызовов:</p><figure><img src="https://media.tproger.ru/uploads/2021/09/Screenshot-2021-09-01-at-01.02.24.png" alt="" /></figure><p>Можно заметить, что при вычислении через рекурсию многие числа будут считаться по несколько раз, например, число 37 будет посчитано 6 раз. В замерах времени выше мы видели, что вычисление этого занимает 11 секунд, т.е. займёт половину времени от вычислений числа 41 — 66 секунд из 135.</p><p>Напрашивается решение проблемы — запоминать вычисленные значения и при необходимости просто их использовать. Давайте посмотрим, что получается в таком случае:</p><p>Пробуем выполнить новую функция с теми же входными данными:</p><p>Во-первых, мы видим, что выполнение теперь занимает доли миллисекунд. Во-вторых, замечаем понижение времени при каждом новом вызове. Это связано с оптимизациями компилятора JavaScript после определенного количества вызова кода. После пары вызовов время уменьшается на 0.1 миллисекунды (для моего компьютера), следовательно, 100 вычислений, которые нам требуются для исходной задачи, пройдут за 0.01 секунду.</p><p>Мы уже применили идеи динамического программирования — сохранили результат вычисления подзадачи для получения результата следующей задачи. Осталось вызвать это в цикле для исходного массива и получить ответ. Но что делать, если в вычисления закрадётся ошибка? Код с рекурсией отлаживать довольно сложно.</p><p>Давайте немного упростим решение за счёт идеи предварительного вычисления всех возможных вариантов. Это займёт очень мало времени, т.к. по условию задачи максимальное число — 100, и за счет идеи отказа от рекурсии. Будем считать так, как бы мы считали эти числа на листике, руками:</p><p>Поскольку идем, увеличивая число i, и у нас изначально добавлено в массив tribonacciNumbersCache три значения, то получение элементов i - 1, i - 2 и i - 3 отработает без ошибок.</p><p>В 0 позиций массива tribonacciNumbersCache — первое число трибонначи, главное не забыть вычитать единицу при получении значения.</p><p>Напишем решение исходной задачи — по массиву номеров получить массив с числами трибонначи:</p><p>В консоль будет выведено [2073693258389777400, 44, 1, 927, 19513, 7]. Задача решена.</p><p>Задачи на LeetCode и CodeWars для тренировки вычислений чисел <a href="https://www.codewars.com/collections/fibonacci-1">фибонначи</a>, <a href="https://leetcode.com/problems/n-th-tribonacci-number/">трибонначи</a>.</p><h2>Основные идеи</h2><p>Мы использовали несколько подходов, типичных для динамического программирования:</p><ol><li>Определили, как из известных данных маленьких подзадач будет вычисляться следующая подзадача, т.е. с числами трибонначи из трёх значений будет вычисляться следующее значение. Это соотношение называется рекурсивным.</li><li>Нашли исходные данные, чтобы можно было запустить наше вычисление.</li><li>Определили, откуда брать ответ — вычитали единицу при обращении к элементу массива с посчитанными значениями.</li></ol><h2>Кузнечик и кувшинки</h2><p>Рассмотрим следующую задачу: есть определённое количество кувшинок, расположенных в ряд, кузнечик стоит на первой из них. Он может прыгнуть на следующую кувшинку, либо перепрыгнуть через одну. Сколько существует разных способов (путей) добраться до последней кувшинки?</p><p>Рассмотрим вариант с 4-мя кувшинками. Можем перебрать варианты и посчитать, что до последней кувшинки можно добраться 3-мя разными способами:</p><figure><img src="https://media.tproger.ru/uploads/2021/09/Screenshot-2021-09-11-at-01.16.52.png" alt="" /></figure><p>Грустный кузнечик прыгает по кувшинкам</p><p>Рассмотрим сколько способов существует, чтобы добраться до каждой из кувшинок. До второй — 1 способ, до третьей — 2 способа: прыгнуть сразу на неё или прыгнуть с первой кувшинки. До четвёртой уже выяснили перебором, что существует 3 способа.<br />Можем заметить, что на четвёртую кувшинку мы можем прыгнуть только со второй или с третьей и что количество путей до неё равно количеству путей до второй кувшинки + количество путей до третьей кувшинки. Немного порассуждаем, почему так получается.</p><p>С третьей кувшинки на четвёртую есть только один путь — прыгнуть на неё, но при этом до третьей кувшинки было 2 разных пути. Соответственно, если мы двигаемся через третью кувшинку на четвёртую, то так и остается 2 разных пути это сделать. Аналогичные рассуждения можно провести для перемещения со второй на четвёртую кувшинку. Эти два случая будут разными путями, т.к. перед попаданием на четвёртой мы были на разных кувшинках, следовательно, количество путей надо просто сложить.</p><p>Аналогичные рассуждения можно провести для дальнейших перемещений, т.е. просто складываем количество путей до предыдущих двух кувшинок.</p><p>Понятно, как переходить от одной подзадачи к другой, осталось найти начальные условия. Возьмём за начальные условия количество вариантов попасть на вторую и третью кувшинки, т.е. 1 и 2.</p><p>Для нахождения начальных условий можно использовать более математический подход: ввести отрицательные кувшинки, количество путей до них будет 0, и использовать текущую позицию как последнее значение начальных условий. При таком подходе начальные условия будут 0 и 1. В текущей задаче будем использовать первый подход, но для более сложных задач имеет смысл использовать второй.</p><p>Получается следующая функция:</p><p>Задача решена, но можем заметить, что все значения, кроме двух последних, не используются, и упростить решение:</p><p>В цикле происходит смещение количества путей до i + 1 кувшинки. В начале way1 указывает на количество путей до первой кувшинки, way2 — до второй. После первого выполнения тела цикла way1 указывает на количество путей до второй кувшинки, way2 до третьей, и так далее. В конце выполнения цикла в way2 получится количество путей до n кувшинки.</p><p><a href="https://leetcode.com/problems/climbing-stairs/">Задача для практики на LeetCode</a></p><h2>Максимальный(по сумме элементов) подмассив</h2><p>Условие задачи: дан массив чисел, требуется найти непрерывную последовательность чисел с максимальной суммой.</p><p>Например, получен такой массив:</p><figure><img src="https://media.tproger.ru/uploads/2021/09/Screenshot-2021-09-17-at-02.54.23.png" alt="" /><figcaption>Массив для нахождения максимального подмассива</figcaption></figure><p>Рассмотрим идею решения. Начнём суммировать значения в этом массиве слева направо. Просуммировав два первых элемента: 2 — 5 = −3, получим отрицательное число. Если добавить это число к следующему, то оно уменьшит его, независимо от того положительное оно или отрицательное. Просуммировав следующие 3 элемента 2 + 2 — 1 = 3, получим положительное значение, которое при добавлении к следующему увеличит сумму с ним в любом случае.</p><p>Т.е. мы идём слева направо, суммируя элементы, если сумма становится отрицательной, то игнорируем подсумму, если положительной, то прибавляем дальше:</p><figure><img src="https://media.tproger.ru/uploads/2021/09/Screenshot-2021-09-17-at-02.52.12.png" alt="" /><figcaption>Суммы, используемые для нахождения максимального подмассива</figcaption></figure><p>В процессе этих вычислений при каждом прибавлении сравниваем число с переменной, в которой хранится максимальное подсумма.<br />Рассмотрим решение в коде:</p><p>Рассмотрим массив arrayOfSums, на каждом шаге итерации:</p><p>Видим, что максимальное значение 7. Это и будет ответ в текущей задаче.</p><p>В решении мы находим только максимальную сумму подмассива, но не находим саму часть массива. По рассмотренному массиву arrayOfSums и логике решения задачи довольно очевидно, как найти требуемый подмассив: находим 7 в массиве arrayOfSums, это будет конец подмассива. Берём слева все положительные числа, самое левое из списка положительных чисел — начало подмассива.</p><p>В текущем случае это сумма от второго до седьмого элемента (при отсчёте массива от 0). Подмассив arrayOfSums [2, 4, 3, 6, 5, 7], подмассив исходного массива nums [2, 2, -1, 3, -1, 2].</p><p>Реализуем эту идею в коде:</p><p><a href="https://leetcode.com/problems/maximum-subarray/">Задача для практики на LeetCode</a></p><h2>Заключение</h2><p>Мы рассмотрели несколько простых задач динамического программирования, желаю успехов в освоении этого непростого, но очень интересного подхода к решению задач.</p>]]></content:encoded>
    </item>
    <item>
      <title>Находим N'е число Фибоначчи тремя способами за приемлемое время: основы динамического программирования</title>
      <link>https://tproger.ru/problems/finding-fibonacci</link>
      <comments>https://tproger.ru/problems/finding-fibonacci?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/problems/finding-fibonacci</guid>
      <description><![CDATA[<p>Простая рекурсия для чисел Фибоначчи съедает ресурсы уже на тридцатом элементе. Три подхода дают ответ за приемлемое для олимпиад время.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/problems/finding-fibonacci">Находим N'е число Фибоначчи тремя способами за приемлемое время: основы динамического программирования</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Задачки]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sun, 13 Nov 2016 20:43:26 GMT</pubDate>
      <content:encoded><![CDATA[<p>Задача: посчитать N-е число последовательности, в которой каждый элемент равен сумме двух предыдущих. Такая последовательность называется последовательностью Фибоначчи: 1, 1, 2, 3, 5, 8…</p><p>Очень часто на разнообразных олимпиадах попадаются задачи вроде этой, которые, как думается на первый взгляд, можно решить с помощью простого перебора. Но если мы подсчитаем количество возможных вариантов, то сразу убедимся в неэффективности такого подхода: например, простая рекурсивная функция, приведенная ниже, будет потреблять существенные ресурсы уже на 30-ом числе Фибоначчи, тогда как на олимпиадах время решения часто ограничено 1-5 секундами.</p><p>Давайте подумаем, почему так происходит. Например, для вычисления fibo(30) мы сначала вычисляем fibo(29) и fibo(28). Но при этом наша программа «забывает», что fibo(28) мы уже вычисляли при поиске fibo(29).</p><p>Основная ошибка такого подхода «в лоб» в том, что одинаковые значения аргументов функции исчисляются многократно — а ведь это достаточно ресурсоемкие операции. Избавиться от повторяющихся вычислений нам поможет метод динамического программирования — это прием, при использовании которого задача разбивается на общие и повторяющиеся подзадачи, каждая из которых решается только 1 раз — это значительно повышает эффективность программы. Этот метод подробно описан в <a href="https://tproger.ru/articles/dinprog-starters/">нашей статье</a>, там же есть и примеры решения других задач.</p><p>Самый просто вариант улучшения нашей функции — запоминать, какие значения мы уже вычисляли. Для этого нужно ввести дополнительный массив, который будет служить как бы «кэшем» для наших вычислений: перед вычислением нового значения мы будем проверять, не вычисляли ли его раньше. Если вычисляли, то будем брать из массива готовое значение, а если не вычисляли — придётся считать его на основе предыдущих и запоминать на будущее:</p><p>Так как в данной задаче для вычисления N-ого значения нам гарантированно понадобится (N-1)-е, то не составит труда переписать формулу в итерационный вид — просто будем заполнять наш массив подряд до тех пор, пока не дойдём до нужной ячейки:</p><p>Теперь мы можем заметить, что когда мы вычисляем значение F(N), то значение F(N-3) нам уже гарантированно никогда не понадобится. То есть нам достаточно хранить в памяти лишь два значения — F(N-1) и F(N-2). Причём, как только мы вычислили F(N), хранение F(N-2) теряет всякий смысл. Попробуем записать эти размышления в виде кода:</p><p>Бывалому программисту понятно, что код выше, в общем-то ерунда, так как cache3 никогда не используется (он сразу записывается в cache2), и всю итерацию можно переписать, используя всего одно выражение:</p><p>Для тех, кто не может понять, как работает магия с остатком от деления, или просто хочет увидеть более неочевидную формулу, существует ещё одно решение:</p><p>Попробуйте проследить за выполнением этой программы: вы убедитесь в правильности алгоритма.</p><p>P.S. Вообще, существует единая формула для вычисления любого числа Фибоначчи, которая не требует никаких итераций или рекурсии:</p><p>Но, как можете догадаться, подвох в том, что цена вычисления степеней нецелых чисел довольно велика, как и их погрешность.</p>]]></content:encoded>
    </item>
    <item>
      <title>Динамическое программирование для начинающих</title>
      <link>https://tproger.ru/articles/dynprog-starters</link>
      <comments>https://tproger.ru/articles/dynprog-starters?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Картофельный Повелитель]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/dynprog-starters</guid>
      <description><![CDATA[<p>Пошаговое руководство по динамическому программированию для начинающих. Рекурсия, мемоизация, последовательное вычисление, одномерная и двумерная динамика — с разбором классических задач и примерами кода на Java.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/dynprog-starters">Динамическое программирование для начинающих</a>»</p>]]></description>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 07 Jun 2016 12:04:43 GMT</pubDate>
      <content:encoded><![CDATA[<p>Динамическое программирование — тема, которой в рунете посвящено довольно мало статей, поэтому мы решили ею заняться. В этой статье будут разобраны классические задачи на последовательности, одномерную и двумерную динамику, будет дано обоснование решениям и описаны разные подходы к их реализации. Весь приведённый в статье код написан на Java.</p><p><b>Ключевые моменты статьи:</b><br />- Динамическое программирование — метод разбиения задачи на подзадачи, связанные рекуррентной формулой<br />- Четыре способа реализации: рекурсия, рекурсия с мемоизацией, последовательное вычисление, стек индексов<br />- Мемоизация устраняет повторные вычисления за счёт расхода памяти<br />- Одномерная динамика — последовательности и задачи вроде подъёма по лестнице<br />- Двумерная динамика — задачи на таблицах, например поиск кратчайшего пути в сетке</p><h2>О чём вообще речь? Что такое динамическое программирование?</h2><p>Динамическое программирование — метод решения задачи путём её разбиения на несколько одинаковых подзадач, рекуррентно связанных между собой. Самым простым примером будут <a href="https://ru.wikipedia.org/wiki/Числа_Фибоначчи">числа Фибоначчи</a> — чтобы вычислить некоторое число в этой последовательности, нам нужно сперва вычислить третье число, сложив первые два, затем четвёртое таким же образом на основе второго и третьего, и так далее (да, мы слышали про замкнутую формулу).</p><h2>Хорошо, как это использовать?</h2><p>Решение задачи динамическим программированием должно содержать следующее:</p><ul><li>Зависимость элементов динамики друг от друга. Такая зависимость может быть прямо дана в условии (так часто бывает, если это задача на числовые последовательности). В противном случае вы можете попытаться узнать какой-то известный числовой ряд (вроде тех же чисел Фибоначчи), вычислив первые несколько значений вручную. Если вам совсем не повезло — придётся думать</li><li>Значение начальных состояний. В результате долгого разбиения на подзадачи вам необходимо свести функцию либо к уже известным значениям (как в случае с Фибоначчи — заранее определены первые два члена), либо к задаче, решаемой элементарно.</li></ul><figure><img src="https://media.tproger.ru/uploads/2016/06/399_v1.png" alt="Схема разбиения задачи на подзадачи в динамическом программировании" /></figure><h2>И что, мне для решения рекурсивный метод писать надо? Я слышал, они медленные.</h2><p>Конечно, не надо, есть и другие подходы к реализации динамики. Разберём их на примере следующей задачи:</p><blockquote>Вычислить n-й член последовательности, заданной формулами:<br />a2n = an + an-1,<br />a2n+1 = an — an-1,<br />a0 = a1 = 1.</blockquote><h3>Идея решения</h3><p>Здесь нам даны и начальные состояния (a0 = a1 = 1), и зависимости. Единственная сложность, которая может возникнуть — понимание того, что 2n — условие чётности числа, а 2n+1 — нечётности. Иными словами, нам нужно проверять, чётно ли число, и считать его в зависимости от этого по разным формулам.</p><h3>Рекурсивное решение</h3><p>Очевидная реализация состоит в написании следующего метода:</p><p>И она отлично работает, но есть нюансы. Если мы захотим вычислить f(12), то метод будет вычислять сумму f(6)+f(5). В то же время, f(6)=f(3)+f(2) и f(5)=f(2)-f(1), т.е. значение f(2) мы будем вычислять дважды. Спасение от этого есть — мемоизация (кеширование значений).</p><h3>Рекурсивное решение с кэшированием значений</h3><p>Идея мемоизации очень проста — единожды вычисляя значение, мы заносим его в какую-то структуру данных. Перед каждым вычислением мы проверяем, есть ли вычисляемое значение в этой структуре, и если есть, используем его. В качестве структуры данных можно использовать массив, заполненный флаговыми значениями. Если значение элемента по индексу N равно значению флага, значит, мы его ещё не вычисляли. Это создаёт определённые трудности, т.к. значение флага не должно принадлежать множеству значений функции, которое не всегда очевидно. Лично я предпочитаю использовать <a href="https://ru.wikipedia.org/wiki/Хеш-таблица">хэш-таблицу</a> — все действия в ней выполняются за O(1), что очень удобно. Однако, при большом количестве значений два числа могут иметь одинаковый хэш, что, естественно, порождает проблемы. В таком случае стоит использовать, например, <a href="https://ru.wikipedia.org/wiki/Красно-чёрное_дерево">красно-чёрное дерево</a> (подробнее о деревьях — в статье про <a href="https://tproger.ru/translations/binary-search-tree-for-beginners">двоичное дерево поиска</a>).</p><p>Для уже написанной функции f(int) кэширование значений будет выглядеть следующим образом:</p><p>Не слишком сложно, согласитесь? Зато это избавляет от огромного числа операций. Платите вы за это лишним расходом памяти.</p><h3>Последовательное вычисление</h3><p>Теперь вернёмся к тому, с чего начали — рекурсия работает медленно. Не слишком медленно, чтобы это приносило действительные неприятности в настоящей жизни, но на соревнованиях по спортивному программированию каждая миллисекунда на счету.</p><p>Метод последовательного вычисления подходит, только если функция ссылается исключительно на элементы перед ней — это его основной, но не единственный минус. Наша задача этому условию удовлетворяет.</p><p>Суть метода в следующем: мы создаём массив на N элементов и последовательно заполняем его значениями. Вы, наверное, уже догадались, что таким образом мы можем вычислять в том числе те значения, которые для ответа не нужны. В значительной части задач на динамику этот факт можно опустить, так как для ответа часто бывают нужны как раз все значения. Например, при поиске наименьшего пути мы не можем не вычислять путь до какой-то точки, нам нужно пересмотреть все варианты. Но в нашей задаче нам нужно вычислять приблизительно log2(N) значений (на практике больше), для 922337203685477580-го элемента (MaxLong/10) нам потребуется 172 вычисления.</p><p>Ещё одним минусом такого подхода является сравнительно большой расход памяти.</p><h3>Создание стека индексов</h3><p>Сейчас нам предстоит, по сути, написать свою собственную рекурсию. Идея состоит в следующем — сначала мы проходим «вниз» от N до начальных состояний, запоминая аргументы, функцию от которых нам нужно будет вычислять. Затем возвращаемся «вверх», последовательно вычисляя значения от этих аргументов, в том порядке, который мы записали.</p><p>Зависимости вычисляются следующим образом:</p><p>Полученный размер стека — то, сколько вычислений нам потребуется сделать. Именно так я получил упомянутое выше число 172.</p><p>Теперь мы поочередно извлекаем индексы и вычисляем для них значения по формулам — гарантируется, что все необходимые значения уже будут вычислены. Хранить будем как раньше — в хэш-таблице.</p><p>Все необходимые значения вычислены, осталось только написать</p><p>Конечно, такое решение гораздо более трудоёмкое, однако это того стоит.</p><h2>Хорошо, математика — это красиво. А что с задачами, в которых не всё дано?</h2><p>Для большей ясности разберём следующую задачу на одномерную динамику:</p><blockquote>На вершине лесенки, содержащей N ступенек, находится мячик, который начинает прыгать по ним вниз, к основанию. Мячик может прыгнуть на следующую ступеньку, на ступеньку через одну или через 2. (То есть, если мячик лежит на 8-ой ступеньке, то он может переместиться на 5-ую, 6-ую или 7-ую.) Определить число всевозможных «маршрутов» мячика с вершины на землю.</blockquote><h3>Идея решения</h3><p>На первую ступеньку можно попасть только одним образом — сделав прыжок с длиной равной единице. На вторую ступеньку можно попасть сделав прыжок длиной 2, или с первой ступеньки — всего 2 варианта. На третью ступеньку можно попасть сделав прыжок длиной три, с первой или со втрой ступенек. Т.е. всего 4 варианта (0-&gt;3; 0-&gt;1-&gt;3; 0-&gt;2-&gt;3; 0-&gt;1-&gt;2-&gt;3). Теперь рассмотрим четвёртую ступеньку. На неё можно попасть с первой ступеньки — по одному маршруту на каждый маршрут до неё, со второй или с третьей — аналогично. Иными словами, количество путей до 4-й ступеньки есть сумма маршрутов до 1-й, 2-й и 3-й ступенек. Математически выражаясь, F(N) = F(N-1)+F(N-2)+F(N-3). Первые три ступеньки будем считать начальными состояниями.</p><h3>Реализация через рекурсию</h3><p>Здесь ничего хитрого нет.</p><h3>Реализация через массив значений</h3><p>Исходя из того, что, по большому счёту, простое решение на массиве из N элементов очевидно, я продемонстрирую тут решение на массиве всего из трёх.</p><p>Так как каждое следующее значение зависит только от трёх предыдущих, ни одно значение под индексом меньше i-3 нам бы не пригодилось. В приведённом выше коде мы записываем новое значение на место самого старого, не нужного больше. Цикличность остатка от деления на 3 помогает нам избежать кучи условных операторов. Просто, компактно, элегантно.</p><h2>Там вверху ещё было написано про какую-то двумерную динамику?...</h2><p>С двумерной динамикой не связано никаких особенностей, однако я, на всякий случай, рассмотрю здесь одну задачу и на неё.</p><blockquote>В прямоугольной таблице NxM в начале игрок находится в левой верхней клетке. За один ход ему разрешается перемещаться в соседнюю клетку либо вправо, либо вниз (влево и вверх перемещаться запрещено). Посчитайте, сколько есть способов у игрока попасть в правую нижнюю клетку.</blockquote><h3>Идея решения</h3><p>Логика решения полностью идентична таковой в задаче про мячик и лестницу — только теперь в клетку (x,y) можно попасть из клеток (x-1,y) или (x, y-1). Итого F(x,y) = F(x-1, y)+F(x,y-1). Дополнительно можно понять, что все клетки вида (1,y) и (x,1) имеют только один маршрут — по прямой вниз или по прямой вправо.</p><h3>Реализация через рекурсию</h3><p>Ради всего святого, не нужно делать двумерную динамику через рекурсию. Уже было упомянуто, что рекурсия менее выгодна, чем цикл по быстродействию, так двумерная рекурсия ещё и читается ужасно. Это только на таком простом примере она смотрится легко и безобидно.</p><h3>Реализация через массив значений</h3><p>Классическое решение динамикой, ничего необычного — проверяем, является ли клетка краем, и задаём её значение на основе соседних клеток.</p><h2>Отлично, я всё понял. На чём мне проверить свои навыки?</h2><p>В заключение приведу ряд типичных задач на одномерную и двумерную динамику, разборы прилагаются. Ещё больше задач — в подборке <a href="https://tproger.ru/articles/problems">123 задач с IT-собеседований</a>.</p><h3>Взрывоопасность</h3><blockquote>При переработке радиоактивных материалов образуются отходы двух видов — особо опасные (тип A) и неопасные (тип B). Для их хранения используются одинаковые контейнеры. После помещения отходов в контейнеры последние укладываются вертикальной стопкой. Стопка считается взрывоопасной, если в ней подряд идет более одного контейнера типа A. Стопка считается безопасной, если она не является взрывоопасной. Для заданного количества контейнеров N определить количество возможных типов безопасных стопок.</blockquote><h3>Подъём по лестнице</h3><blockquote>Мальчик подошел к платной лестнице. Чтобы наступить на любую ступеньку, нужно заплатить указанную на ней сумму. Мальчик умеет перешагивать на следующую ступеньку, либо перепрыгивать через ступеньку. Требуется узнать, какая наименьшая сумма понадобится мальчику, чтобы добраться до верхней ступеньки.</blockquote><h3>Калькулятор</h3><blockquote>Имеется калькулятор, который выполняет три операции:Прибавить к числу X единицу;Умножить число X на 2;Умножить число X на 3.Определите, какое наименьшее число операций необходимо для того, чтобы получить из числа 1 заданное число N. Выведите это число, и, на следующей строке, набор исполненных операций вида «111231».</blockquote><h3>Самый дешёвый путь</h3><blockquote>В каждой клетке прямоугольной таблицы N*M записано некоторое число. Изначально игрок находится в левой верхней клетке. За один ход ему разрешается перемещаться в соседнюю клетку либо вправо, либо вниз (влево и вверх перемещаться запрещено). При проходе через клетку с игрока берут столько килограммов еды, какое число записано в этой клетке (еду берут также за первую и последнюю клетки его пути).Требуется найти минимальный вес еды в килограммах, отдав которую игрок может попасть в правый нижний угол.</blockquote><h2>Часто задаваемые вопросы</h2><p><b>Чем динамическое программирование отличается от жадных алгоритмов?</b><br />Жадный алгоритм на каждом шаге выбирает локально оптимальное решение и не пересматривает его. Динамическое программирование рассматривает все варианты через подзадачи и гарантирует глобально оптимальный результат. Жадный подход быстрее, но работает только для задач со свойством жадного выбора. Если жадный алгоритм даёт неправильный ответ на каком-то тесте — скорее всего, задачу нужно решать динамикой.</p><p><b>Когда использовать мемоизацию, а когда табуляцию (массив)?</b><br />Мемоизация (top-down) удобна, когда не все подзадачи нужны для ответа — вычисляются только те, к которым обращается рекурсия. Табуляция (bottom-up) через массив работает быстрее за счёт отсутствия накладных расходов на рекурсию и лучше подходит, когда нужны все промежуточные значения. На практике для большинства задач разница незначительна, но на соревнованиях табуляция предпочтительнее.</p><p><b>Как определить, что задачу можно решить динамическим программированием?</b><br />Два ключевых признака: оптимальная подструктура (оптимальное решение задачи строится из оптимальных решений подзадач) и перекрывающиеся подзадачи (одни и те же подзадачи решаются многократно). Если задача просит найти количество способов, минимальную стоимость или максимальную прибыль — велика вероятность, что это задача на динамику.</p><p><b>Можно ли использовать динамическое программирование на Python вместо Java?</b><br />Да, принципы остаются одинаковыми на любом языке. В Python удобно использовать декоратор @lru_cache из модуля functools для автоматической мемоизации, а словари dict заменяют HashMap. Единственный нюанс — ограничение глубины рекурсии в Python (по умолчанию 1000), которое можно увеличить через sys.setrecursionlimit().</p>]]></content:encoded>
    </item>
    <item>
      <title>123 задачи с IT-собеседований с разбором решений</title>
      <link>https://tproger.ru/articles/problems</link>
      <comments>https://tproger.ru/articles/problems?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Картофельный Повелитель]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/problems</guid>
      <description><![CDATA[<p>Подборка из 123 задач с IT-собеседований с разбором решений. Логика, алгоритмы, программирование.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/problems">123 задачи с IT-собеседований с разбором решений</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Стеки и очереди]]></category>
      <category><![CDATA[Поразрядная обработка]]></category>
      <category><![CDATA[Сортировка и поиск]]></category>
      <category><![CDATA[C++]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Головоломки]]></category>
      <category><![CDATA[Математика и теория вероятностей]]></category>
      <category><![CDATA[Задачи умеренной сложности]]></category>
      <category><![CDATA[Задачи повышенной сложности]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Собеседование]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 20 Oct 2015 12:45:48 GMT</pubDate>
      <content:encoded><![CDATA[<p>При приеме сотрудника в офис на должность программиста, работодатель испытывает кандидата не только вопросами о навыках, но и всевозможными логическими задачами, IT-кейсами и заданиями по разработке для профессиональных программистов. Как правило, список этих задач у работодателей совпадает, а значит, мы можем подготовиться к любому собеседованию!</p><p><b>Ключевые выводы</b>
• На IT-собеседованиях встречаются логические, алгоритмические задачи, вопросы по языкам и Fermi-задачи
• Ключ к успеху — умение рассуждать вслух
• Важно практиковаться на разных типах задач
• Задачи давались в Google, Amazon, Apple и Twitter</p><p>В нашей <a href="https://tproger.ru/problems/">рубрике c заданиями</a> уже больше 100 вопросов с подробным разбором решений. Мы решили собрать воедино все самые интересные и популярные задачи для программистов, которые могут встретиться вам на собеседовании.</p><h2>Задачи на структуры данных</h2><p>Есть однонаправленный список из структур. В нём random указывает на какой-то еще элемент этого же списка. Требуется написать функцию, которая копирует этот список с сохранением структуры (т.е. если в старом списке random первой ноды указывал на 4-ю, в новом списке должно быть то же самое – рандом первой ноды указывает на 4-ю ноду нового списка). O(n), константная дополнительная память + память под элементы нового списка. Нельзя сразу выделить память под все данные одник куском т.е. список должен быть честным, разбросанным по частям, а не единым блоком, как массив.</p><p>Ответ Вариант реализации</p><p>Классическая задачка с собеседований в Google. На доске записаны числа, вам нужно ответить на вопрос: какое число идёт дальше?</p><p>Ответ</p><p>Допустим, вы летите из Москвы во Владивосток, а затем обратно, при полном безветрии. Затем вы совершаете точно такой же перелёт, но на этот раз на протяжении всего перелёта дует постоянный западный ветер: в одну сторону попутный, в обратную — лобовой.</p><p>Ответ</p><p>Что не так в этом отрывке кода на С++?</p><p>Ответ</p><p>Задача, которая была популярна в своё время на собеседованиях в Amazon. Мы русифицировали её, но смысл остался тот же. Вам нужно продолжить последовательность.</p><p>Ответ автора с обзором вариантов ответов подписчиков</p><p>Как это вычислить, не пользуясь калькулятором? Можете дать приблизительный ответ?</p><p>Ответ</p><p>«Вас уменьшили до размеров 5-центовой монеты и бросили в блендер. Ваш вес уменьшился так, что плотность вашего тела осталась прежней. Лезвия начнут вращаться через 60 секунд. Ваши действия?»</p><p>Это классическая google-задачка, хороший разбор которой в рунете не так-то просто найти. Мы подготовили его для вас. Абсолютного правильного ответа нет, но есть те, которые явно лучше остальных.</p><p>Разбор вариантов ответа</p><p>Вопрос по С++. Что за ошибка «pure virtual function call»? В какой ситуации она может быть сгенерирована? Предоставьте минимальный код, приводящий к ней.</p><p>Ответ</p><p>В вашем распоряжении 10 тысяч серверов в дата-центре с возможностью удалённого управления и один день, чтобы получить миллион долларов. Что вы для этого сделаете?</p><p>Ответ</p><p>У вас есть аналоговые часы с секундной стрелкой. Сколько раз в день все три стрелки часов накладываются друг на друга?</p><p>Ответ</p><p>В чём разница между string и String в C#?</p><p>Ответ</p><p>Вы играете в футбол на пустынном острове и хотите подбросить монетку, чтобы решить, какой команде достанется мяч. Единственная монета, что у вас есть, является гнутой, и поэтому вносит явные искажения в результат при подбрасывании. Как вы тем не менее можете использовать такую монету, чтобы принять справедливое решение?</p><p>Ответ</p><p>Cколько мячей для гольфа войдет в школьный автобус?</p><p>Для справки: в Национальных стандартах транспотрных средств для школ в США на 1995 год указаны максимальные размеры школьного автобуса и равны 40 футам в длину и 8.5 футам в ширину. Стандартный диаметр мяча для гольфа — 1.69 дюйма с допуском 0.005 дюймов.</p><p>Ответ</p><p>Представьте себе вращающийся диск, например DVD. У вас есть в распоряжении черная (Ч) и белая (Б) краски. На краю диска установлен небольшой датчик, который определяет цвет под ним и выдает результат в виде сигнала. Как бы вы раскрасили диск, чтобы было возможно определить направление вращения по показаниям датчика?</p><p>Дадим небольшое пояснение к задаче. Первое, что нужно иметь ввиду, это то, что нельзя наблюдать за самим диском. Например, вы сидите в офисе, а диск вращается в закрытой лаборатории. Единственная возможность определить направление вращения — использовать оцифрованные показания датчика, и ничего больше.</p><p>Датчик фиксирует цвет точки в непосредственном месте установки в последовательные моменты времени. Показания представляются в виде «ЧЧЧББ…». Задача сводится к такой раскраске диска, где последовательность показаний отличается при вращении в прямую и в противоположную стороны.</p><p>Ответ</p><p>У вас есть исходный код приложения на языке С, которое аварийно завершается после запуска. После десяти запусков в отладчике вы обнаруживаете, что каждый раз программа падает в разных местах. Приложение однопоточное и использует только стандартную библиотеку С. Какие ошибки могут вызвать падение приложения? Как вы проверите каждую?</p><p>Ответ</p><p>Найдите ошибки в следующем коде.</p><p>Ответ</p><p>Объясните, что делает этот код.</p><p>((n &amp; (n – 1)) == 0)</p><p>Ответ</p><h2>Задачи на логику и математику</h2><p>Дано 100-этажное здание. Если яйцо сбросить с высоты N-го этажа (или с большей высоты), оно разобьется. Если его бросить с любого меньшего этажа, оно не разобьется. У вас есть два яйца. Найдите N за минимальное количество бросков.</p><p>Ответ</p><p>Продолжаем задачки по С/С++. Что означает ключевое слово volatile и в каких ситуация оно может быть применено? Если даже помните формальное значение, попробуйте привести пример ситуации, где volatile на самом деле будет полезно.</p><p>Ответ</p><p>У вас есть отсортированная матрица размера MxN. Предложите алгоритм поиска в ней произвольного элемента. Под отсортированной матрицей будем понимать такую матрицу, строки и столбцы которой отсортированы (см. пример).</p><p>Ответ</p><p>Напишите метод, находящий максимальное из двух чисел, не используя операторы if-else или любые другие операторы сравнения.</p><p>Ответ</p><p>На пустынном шоссе вероятность появления автомобиля за 30-минутный период составляет 0.95. Какова вероятность его появления за 10 минут?</p><p>Ответ</p><p>Напишите функцию суммирования двух целых чисел без использования «+» и других арифметических операторов.</p><p>Ответ</p><p>У вас есть парк из 50 грузовиков. Каждый из них полностью заправлен и может проехать 100 км. Как далеко с их помощью вы можете доставить определенный груз? Что будет, если в вашем распоряжении N грузовиков?</p><p>Не все понимают сразу о чем речь: территориально это место, где нет никаких заправочных станций. Единственное место, где можно здесь найти горючее – это топливные баки грузовиков. Пересесть из грузовика в гибридный легковой автомобиль Prius нельзя. Бросить грузовик без топлива, где бы это ни случилось, и без водителя – в порядке вещей. И единственное, что здесь важно, – доставить как можно дальше ценный груз.</p><p>Ответ</p><p>Опишите алгоритм для нахождения миллиона наименьших чисел в наборе из миллиарда чисел. Память компьютера позволяет хранить весь миллиард чисел. Если придумали какое-либо решение, то оцените его эффективность по времени. Есть ли более эффективное решение?</p><p>Ответ</p><p>Напишите метод, который будет подсчитывать количество цифр «2», используемых в десятичной записи целых чисел от 0 до n (включительно). Картинка дана в качестве подсказки к одному из возможных решений.</p><p>Ответ</p><p>Где вы будете плыть быстрее — в воде или сиропе?</p><p>Это классическая задача с долгой историей, которую обсуждал в своё время еще Исаак Ньютон. Когда-то она использовалась и на IT-собеседованиях в Google (сейчас — <a href="http://habrahabr.ru/post/184008/">нет</a>). Тем не менее предлагаем вам порассуждать над решением.</p><p>Ответ</p><p>Напишите методы для умножения, вычитания и деления целых чисел, используя из арифметических операций только оператор суммирования. Язык реализации не важен, об оптимизации скорости работы и использования памяти также можете не особо беспокоиться. Главное, что можно использовать только сложение. В подобных задачах полезно вспомнить суть математических операций.</p><p>Ответ</p><p>Допустим, вы пишете конвейер, в котором 2 потока, используя общий буфер, обрабатывают данные. Поток-producer эти данные создает, а поток-consumer их обрабатывает (Producer–consumer problem). Следующий код представляет собой самую простую модель: с помощью std::thread мы порождаем поток-consumer, a создавать данные мы будем в главном потоке.</p><p>Опустим механизмы синхронизации двух потоков, и обратим внимание на функцию main(). Попробуйте догадаться, что с этим кодом не так, и как его исправить?</p><p>Ответ</p><p>Дано 20 баночек с таблетками. В 19 из них лежат таблетки весом 1 г, а в одной – весом 1.1 г. Даны весы, показывающие точный вес. Как за одно взвешивание найти банку с тяжелыми таблетками?</p><p>Ответ</p><p>Дана шахматная доска размером 8×8, из которой были вырезаны два противоположных по диагонали угла, и 31 кость домино; каждая кость домино может закрыть два квадратика на поле. Можно ли вымостить костями всю доску? Дайте обоснование своему ответу.</p><p>Ответ</p><p>Дан входной файл, содержащий четыре миллиарда целых 32-битных чисел. Предложите алгоритм, генерирующий число, отсутствующее в файле. Имеется 1 Гбайт памяти для этой задачи. Дополнительно: а что если у вас всего 10 Мбайт? Количество проходов по файлу должно быть минимальным.</p><p>Ответ</p><p>Предложите алгоритм, генерирующий все корректные комбинации пар круглых скобок. Под корректными комбинациями пар будем понимать правильно открытые и закрытые скобки. На вход подаётся число пар скобок, на выходе должны быть все возможные их комбинации в виде набора строк.</p><p>Ответ</p><p>Вы поставили стакан воды на диск проигрывателя виниловых пластинок и медленно увеличиваете скорость вращения. Что произойдет раньше: стакан сползет в сторону, стакан опрокинется, вода расплескается?</p><p>Этот вопрос задавали ранее на собеседованиях в Apple. При ответе рассмотрите возможные варианты и укажите, от чего зависит ответ, если их несколько.</p><p>Ответ</p><p>Короткая задачка по С++ в виде вопроса для новичков. Почему деструктор полиморфного базового класса должен объявляться виртуальным? Полиморфным считаем класс, в котором есть хотя бы одна виртуальная функция.</p><p>Ответ</p><p>Напишите функцию, меняющую местами значения переменных, не используя временные переменные. Предложите как можно больше вариантов.</p><p>Ответ</p><h2>Задачи на алгоритмы</h2><p>Предложите алгоритм поиска в односвязном списке k-го элемента с конца. Список реализован вручную, есть только операция получения следующего элемента и указатель на первый элемент. Алгоритм, по возможности, должен быть оптимален по времени и памяти.</p><p>Ответ</p><p>Напишите функцию, определяющую количество битов, которые необходимо изменить, чтобы из целого числа А получить целое число B. Числа, допустим, 32-битные, язык любой.</p><p>Это одна из типичных задач на работу с битами, которые любят давать на собеседовании. Если вы никогда с ними не сталкивались, вам будет сложно сразу решить задачу с учётом стрессовой ситуации, поэтому запомните использованные при решении трюки.</p><p>Ответ</p><p>В книге N страниц, пронумерованных как обычно от 1 до N. Если сложить количество цифр, содержащихся в каждом номере страницы, будет 1095. Сколько страниц в книге?</p><p>Ответ</p><p>Задачка по С++, которая, тем не менее, будет полезна и для других языков. Сопоставьте хэш-таблицу и mар из стандартной библиотеки шаблонов (STL). Как организована хэш-таблица? Какая структура данных будет оптимальной для небольших объемов данных?</p><p>Ответ</p><p>Разработайте класс, обеспечивающий блокировку так, чтобы предотвратить возникновение <a href="https://ru.wikipedia.org/wiki/%D0%92%D0%B7%D0%B0%D0%B8%D0%BC%D0%BD%D0%B0%D1%8F_%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0">мертвой блокировки</a>.</p><p>Ответ</p><p>Напишите функцию на С++, выводящую в стандартный поток вывода K последних строк файла. При этом файл очень большой, допустим 50 ГБ, длина каждой строки не превышает 256 символов, а число K &lt; 1000.</p><p>Ответ</p><p>Дан кусок сыра в форме куба и нож. Какое минимальное количество разрезов потребуется сделать, чтобы разделить этот кусок на 27 одинаковых кубиков? А на 64 кубика? После каждого разреза части можно компоновать как угодно.</p><p>Такую задачку раньше часто давали на собеседованиях, а придумана она была ещё в 1950 году.</p><p>Ответ</p><p>Реализуйте метод, определяющий, является ли одна строка перестановкой другой. Под перестановкой понимаем любое изменение порядка символов. Регистр учитывается, пробелы являются существенными.</p><p>Ответ</p><p>В тёмной комнате вам вручают колоду карт, в которой известное количество карт N лежат рубашкой вверх, а остальные — вниз. Вы не можете видеть карты, но можете их переворачивать. Как вы разделите колоду на две стопки, чтобы в каждой из них было одинаковое число карт, лежащих рубашкой вверх?</p><p>Эта головоломка в своё время была популярна в JP Morgan Chase. Понятное дело, оказавшись в темноте, вы просто достанете сотовый телефон и воспользуетесь экраном как фонариком. Однако эта задачка появилась до эпохи сотовых телефонов, и её можно решить, даже не видя карт.</p><p>Ответ</p><p>Реализуйте вручную стек со стандартными функциями push/pop и дополнительной функцией min, возвращающей минимальный элемент стека. Все эти функции должны работать за O(1). Решение оптимизируйте по использованию памяти.</p><p>Ответ</p><p>У скольких целых чисел, лежащих в диапазоне от 1 до 1000, есть цифра 3? Посчитать нужно без использования компьютера, приведя свои рассуждения в комментариях.</p><p>Ответ</p><p>У вас есть много URL-адресов, порядка 10 миллиардов. Как бы вы организовали эффективный поиск дубликатов, учитывая, что все они, конечно же, не поместятся в памяти?</p><p>Ответ</p><p>Вы должны выбрать одну из двух ставок. При первом варианте вы должны забросить баскетбольный мяч в корзину за один бросок. Если попадёте, то получите 50 тыс. рублей. Во втором варианте вам надо попасть два раза из трёх бросков, и тогда вы также получите те же 50 тыс. рублей. Какой из этих вариантов вы предпочтёте? Будет ли ваше умение забрасывать мячи влиять на выбор?</p><p>Ответ</p><p>Представьте себе треугольник, составленный из чисел. Одно число расположено в вершине. Ниже размещено два числа, затем три, и так до нижней грани. Вы начинаете на вершине, и нужно спуститься к основанию треугольника. За каждый ход вы можете спуститься на один уровень и выбрать между двумя числами под текущей позицией. По ходу движения вы «собираете» и суммируете числа, которые проходите. Ваша цель — найти максимальную сумму, которую можно получить из различных маршрутов.</p><p>Какой алгоритм вы предложите? Какая у него будет сложность и можно ли предложить лучший вариант?</p><p>Ответ</p><p>Даны два слова или фразы, и ваша задача — проверить, являются ли они анаграммами.</p><p>Анаграмма — это игра со словами, когда в результате перестановки букв слова или фразы получаем другое слово или фразу. Два слова являются анаграммами, если мы можем получить одно из другого переставляя буквы местами.</p><p>Ответ</p><p>Предложите алгоритм, который обнуляет столбец N и строку M матрицы, если элемент в ячейке (N, M) нулевой. Конечно же, нужно минимизировать затраты памяти и время работы.</p><p>Ответ</p><p>Разработайте алгоритм, обнаруживающий в массиве все пары целых чисел, сумма которых равна заданному значению.</p><p>Ответ</p><p>Предположим, вам нужно разработать алгоритм, демонстрирующий круг знакомств человека для социальных сетей. Как бы вы это сделали, при условии, что база очень большая?</p><p>Под большой базой подразумевается порядка миллиарда зарегистрированных пользователей и не менее 100 миллиардов «дружеских» связей между ними.</p><p>Ответ</p><h2>Задачи на графы и списки</h2><p>Допустим, у вас есть однонаправленный список с петлёй. Его «последний» элемент содержит указатель на один из элементов этого же списка, причём не обязательно на первый. Ваша задача — найти начальный узел петли.</p><p>Элементы списка менять нельзя, память можно использовать только константную.</p><p>Ответ</p><p>На острове существует правило — голубоглазые люди не могут там находиться. Самолет улетает с острова каждый вечер в 20:00. Все жители собираются за круглым столом ежедневно, каждый человек может видеть цвет глаз других людей, но не знает цвет собственных. Никто не имеет права сказать человеку, какой у него цвет глаз. На острове находится не менее одного голубоглазого человека. Сколько дней потребуется, чтобы все голубоглазые уехали?</p><p>Ответ</p><p>Напишите код, удаляющий дубликаты из несортированного связного списка. Можно использовать только константную память.</p><p><a></a></p><p>Ответ</p><p>Напишите метод, тасующий карточную колоду. Колода должна быть идеально перемешана т.е. перестановки карт должны быть равновероятными. Вы можете использовать идеальный генератор случайных чисел.</p><p>Ответ</p><p>Допустим, вам поручили задачу по разработке поискового робота — программы, которая, грубо говоря, посещает страницы в Интернете, индексирует, выделяет из них ссылки, переходит по ним и повторяет процесс. Вопрос: как избежать зацикливания?</p><p>Ответ</p><p>У вас есть стеклянный кувшин, в котором лежат небольшие шарики, и вы в любое время можете определить их количество. Вы со своим другом играете в следующую игру: каждый из вас по очереди забирает из кувшина 1 или 2 шарика. Игрок, который забирает последний шарик, выигрывает. Какая самая лучшая стратегия в этой игре? Можете ли вы в самом начале предсказать, кто выиграет?</p><p>Ответ</p><p>Имеется N компаний, и вы хотите, чтобы они слились и образовали одну крупную компанию. Сколько разных способов вы можете использовать для этого? Поглощение можно считать частным случаем слияния, когда А поглощает Б и Б полгощает А — два разных способа. Равнозначные слияния тоже возможны.</p><p>Ответ</p><p>Какой минимальный комплект монет необходим для того, чтобы выдать любую сдачу от 1 до 99 центов? Доступные номиналы монет: 1, 5, 10, 25, 50 центов и 1 доллар.</p><p>Ответ</p><p>У вас есть 25 лошадей. Сколько забегов вам нужно устроить, чтобы определить трех самых быстрых из них? Вы не можете пользоваться секундомером. В каждом заезде могут участвовать только пять лошадей.</p><p>Ответ</p><p>Короткая задачка на сообразительность. По результатам исследования известно, что 70% людей любят кофе, в то же время 80% любят чай. Каковы верхние и нижние границы доли людей, которые одновременно любят кофе и чай?</p><p>Ответ</p><p>Задачка, которую нужно решать без калькулятора и компьютера, имея под рукой только карандаш и бумагу. Сколько нулей в конце факториала 100?</p><p>Ответ</p><p>Предложите алгоритм нахождения самой большой суммы непрерывной последовательности из массива целых чисел, как положительных, так и отрицательных.</p><p>Ответ</p><p>Напишите программу расчета значения медианы в потоке чисел, динамически отслеживающую новые поступающие числа, получаемые рандомом.</p><p>Ответ</p><p>Идет дождь, а вам надо добраться до вашей машины, которая стоит в самом дальнем конце парковки. Побежите ли вы к ней или нет, если ваша цель — как можно меньше промокнуть? Как вы будете себя вести, если у вас есть зонтик?</p><p>Ответ</p><p>Представьте, что существует квадратная матрица, каждый пиксель которой может быть черным или белым. Разработайте алгоритм поиска максимального субквадрата, у которого все стороны черные.</p><p>Разбор <a href="https://tproger.ru/problems/max-black-bordered-submatrix/">двух вариантов решения</a> за O(N^4) и O(N^3). Можете ли вы найти другие варианты?</p><p>Ответ</p><p>Предположим, в некоторый бар ходят только необщительные посетители. Вдоль барной стойки расположены 25 мест. Всякий раз, когда входит новый посетитель, он обязательно садится на самое дальнее, насколько это возможно, место от остальных гостей. Ни один не сядет рядом с кем-то другим: если посетитель входит и видит, что «свободных» мест нет, он тут же разворачивается и уходит из бара. Бармену, естественно, хочется, чтобы за стойкой сидело как можно больше клиентов. Если ему разрешено усадить первого посетителя на любое место, куда выгоднее его посадить с точки зрения бармена?</p><p>Ответ</p><p>Опишите, как можно использовать один одномерный массив для реализации трех стеков.</p><p>Ответ</p><p>У вас есть неограниченное количество монет достоинством 25, 10, 5 и 1 цент. Напишите код, определяющий количество способов представления n центов.</p><p>Ответ</p><p>Напишите код, который позволяет найти минимальное расстояние (выражаемое количеством слов) между любыми двумя словами в файле. Порядок не важен.</p><p>Достаточно ли будет линейного времени?<br />Сколько памяти понадобится для решения?</p><p>Ответ</p><p>Смоделируйте использование игральной кости с семью гранями, если в вашем распоряжении имеется только кость с пятью гранями.</p><p>Иными словами, как получить случайное число в диапазоне от 1 до 7, используя генератор случайных целых чисел от 1 до 5?</p><p>Ответ</p><h2>Задачи на строки и массивы</h2><p>Напишите код, разбивающий связный список вокруг некоторого значения так, чтобы все меньшие узлы оказались перед узлами, большими или равными этому значению.</p><p>Ответ</p><p>Напишите метод, генерирующий случайную последовательность m целых чисел из массива размером n. Все элементы выбираются с одинаковой вероятностью.</p><p>Первое, что приходит в голову, — выбрать случайные элементы из массива и поместить их в новый массив. Но что если мы выберем один и тот же элемент дважды?</p><p>Ответ</p><p>Представьте себе робота, находящегося в левом верхнем углу сетки с координатами (X, Y). Робот может перемещаться в двух направлениях: вправо и вниз. Сколько существует маршрутов, проходящих от точки (0, 0) до точки (X, Y)?</p><p>Дополнительно предположите, что на сетке существуют области, которые робот не может пересекать. Разработайте алгоритм построения маршрута от левого верхнего до правого нижнего угла.</p><p>Ответ</p><p>Реализуйте метод сжатия строки на основе счетчика повторяющихся символов. Например, строка aabcccccaaa должна превратиться в а2b1с5аЗ. Если «сжатая» строка оказывается длиннее исходной, метод должен вернуть исходную строку.</p><p>Ответ</p><p>Вы находитесь в автомобиле, где к полу веревочкой привязан шар, наполненный гелием. Окна закрыты. Вы нажимаете на педаль газа. Что произойдет с шаром: переместится он вперед, назад или останется в прежнем положении?</p><p><a></a></p><p>Ответ</p><p>Задачка, на примере который можно кратко ознакомиться с основами RSA-криптографии.</p><p>Допустим, вы хотите удостовериться, что у вашего друга Пети есть номер вашего телефона. Но вы не можете спросить его об этом прямо. Вам придется написать ему сообщение на карточке и отдать карточку Кате, которая будет выступать в качестве посредника. Катя отнесет карточку Пете, он напишет свое сообщение и отдаст его Кате, которая передаст его вам. Вы не хотите, чтобы Катя узнала ваш номер телефона. Как в таких обстоятельствах следует сформулировать свой вопрос Пете?</p><p>Даже не зная ничего про RSA можно попробовать придумать ответ.</p><p>Ответ</p><p>Задача на знание конкретных языков. Объясните разницу между шаблонами в C++ и дженериками в Java.</p><p>Многие программисты полагают, что шаблоны C++ и дженерики (например в Java) — это одно и то же, ведь их синтаксис похож: в обоих случаях можно написать что-то вроде List&lt;T&gt;. Но различия на самом деле есть.</p><p>Ответ</p><p>Реализуйте вручную «умный» указатель с автоматическим управлением памятью на C++.</p><p>Умный (интеллектуальный) указатель — это тот же обычный указатель, обеспечивающий безопасность благодаря автоматическому управлению памятью. Такой указатель помогает избежать множества проблем: «висячие» указатели, «утечки» памяти и отказы в выделении памяти. Интеллектуальный указатель должен подсчитывать количество ссылок на указанный объект.</p><p>На первый взгляд эта задача кажется довольно сложной, особенно если вы не эксперт в C++.</p><p>Ответ</p><p>Эта головоломка, в которой вас пытаются запутать, предложив поменять свое решение, известна также под именем «Парадокс Монти Холла». Монти Холл был первым ведущим телевизионной игры-шоу «<a href="https://en.wikipedia.org/wiki/Let%27s_Make_a_Deal">Давайте заключим сделку</a>»</p><p>Перед вами три коробки, в одной из которых находится ценный приз, в двух других ничего нет. Вы можете выбрать любую коробку, но вам по-прежнему неизвестно, в какой именно приз. Одну из двух не выбранных вами коробок открывают и показывают, что она пустая. Теперь вы можете или оставить коробку, которую вы первоначально выбрали (оставить), или поменять ее на другую, неоткрытую (заменить). Что вы предпочтете сделать (оставить или заменить)?</p><p>Ответ</p><p>У вас есть пустое помещение и группа людей снаружи. За один ход вы можете либо позволить одному человеку войти в помещение, либо выпустить из него одного человека. Можете ли вы предложить серию ходов, при которой каждая возможная комбинация людей находится в помещении только один раз?</p><p>Нужно время, чтобы понять, чего именно хочет от вас интервьюер. Разобраться в этом помогает простой пример. Скажем, за порогом находятся два человека, Ларри и Сергей. Возможны четыре комбинации их присутствия в комнате, учитывая тот случай, когда в комнате вообще никого нет.</p><p>Вот они:</p><ul><li>В помещении никого нет.</li><li>В помещении только Ларри.</li><li>В помещении только Сергей.</li><li>В помещении Ларри и Сергей.</li></ul><p>Вопрос заключается в том, можем ли мы начать с того, что в комнате никого нет, а затем пройти указанную последовательность шагов?</p><p>Иными словами, как сгенерировать неповторяющиеся комбинации, меняя только один элемент за раз?</p><p>Ответ</p><p>Напишите код поиска субматрицы с максимально возможной суммой в матрице N*N, содержащей положительные и отрицательные числа.</p><p>Ответ</p><p>Сложная задача, требующая умения придумывать алгоритмы.</p><p>По условию требуется разработать алгоритм, позволяющий найти k-e число из упорядоченного числового ряда, в разложении элементов которого на простые множители присутствуют только 3, 5 и 7.</p><p>Решение с примерами кода на Java есть у нас на сайте.</p><p>Решение с примерами кода</p><p>Дан список из миллиона слов. Разработайте алгоритм, создающий максимально возможный прямоугольник из букв так, чтобы каждая строка и каждый столбец образовывали слово (при чтении слева направо и сверху вниз). Слова могут выбираться в любом порядке, строки должны быть одинаковой длины, а столбцы — одинаковой высоты.</p><p>Ответ</p><p>Дан массив с числами типа Integer. Вам нужно написать функцию, которая на входе получит исходный массив, а на выходе вернет массив, в котором каждое значение получено путем произведения всех значений исходного массива с отличным от текущего индексом.</p><p>Для ясности приведем пример. Допустим, исходный массив имеет вид:</p><p>Тогда функция должна вернуть:</p><p>Расчет значений происходит следующим образом:</p><p>Дополнительные условия:</p><ul><li>Нельзя использовать деление.</li><li>Функция должна быть с наименьшими затратами памяти и времени выполнения.</li></ul><p>Ответ</p><h2>Задачи на рассуждение</h2><p>Задача на умение рассуждать. Конкретный ответ не важен, важно показать как вы мыслите. Представьте, что вам необходимо добраться из точки A в точку B, но вы не знаете, как. Как вы будете действовать?</p><p>Ответ</p><p>Представьте, что вам дали задание разработать план эвакуации большого города (в классическом варианте — Сан-Франциско). С чего вы начнете?</p><p>Ответ</p><p>Задача, которую предлагали на собеседованиях в Apple: у вас есть массив с целыми числами, в том числе и отрицательными, вам нужно найти самое большое произведение 3 чисел из этого массива.</p><p>Например: у вас есть массив list_of_ints, содержащий числа -10, -10, 1, 3, 2. Функция, которая обрабатывает этот массив, должна вернуть 300, так как -10 * -10 * 3 = 300. Задание нужно выполнить максимально эффективно, не забывая учесть отрицательные числа.</p><p>Ответ</p><p>Представьте страну, где все родители хотят иметь мальчика. Каждая семья продолжает рожать детей до тех пор, пока у них не появляется мальчик, а затем останавливается. Каково соотношение мальчиков и девочек в этой стране?</p><p>Игнорируйте ситуации, когда рождаются двойняшки, тройняшки и так далее, пары, не имеющие детей, и пары, умершие до того, как у них появится мальчик.</p><p>Как обычно, предлагаем порассуждать над решением в комментариях. Проверить свой ответ можно на сайте по прикреплённой ссылке, там мы даём наш вариант решения.</p><p>Ответ</p><p>Задачи на прикидку, то есть подразумевающие приближенное решение — популярный класс задач, которые предлагают на собеседованиях в IT компании. Предлагаем вам несколько таких задач, а также <a href="https://tproger.ru/problems/fermi/">рассказ</a> об общих методах их решения и <a href="https://tproger.ru/problems/fermi/#cheats">конкретные советы</a> для собеседований.</p><p>Сколько флаконов шампуня производится в мире за год? Ответ</p><p>Сколько насечек на ребре четвертака — монеты в 25 центов? Ответ</p><p>Сколько будет 2 в 64 степени? Ответ</p><p>Сколько туалетной бумаги потребуется, чтобы покрыть ею весь штат? Ответ</p><p>Сколько атомов резины стираются с шины автомобильного колеса при каждом его обороте? Ответ</p><p>Cколько денег понадобится на мытье всех окон в Сиэтле? Ответ</p><p>Также смотрите <a href="https://tproger.ru/problems/fermi/#problems">примеры других задач</a> для самостоятельного решения.</p><p>Задача, которую давали на собеседованиях в Apple. От вас требуется написать функцию, которая возвращает максимальную прибыль от одной сделки с одной акцией (сначала покупка, потом продажа). Исходные данные — массив вчерашних котировок stock_prices_yesterday с ценами акций Apple.<br />Информация о массиве:</p><ul><li>Индекс равен количеству минут с начала торговой сессии (9:30 утра).</li><li>Значение в массиве равно стоимости акции в это время.</li></ul><p>Например: если акция в 10:00 утра стоила 20 долларов, то<br />stock_prices_yesterday[30] = 20.</p><p>Допустим, имеем некоторые условия:</p><p>Массив может быть любым, хоть за весь день. Нужно написать функцию get_max_profit как можно эффективнее — с наименьшими затратами времени выполнения и памяти.</p><p>Ответ</p><p>Задача про слияние промежутков в календаре.</p><p>Предположим, компания, в которой вы работаете, разрабатывает электронный календарь. В календаре есть функция, показывающая, когда различные команды программистов будут заняты на какой-либо встрече.</p><p>Те периоды, когда команда занята, на календаре отмечены как диапазоны времени, например, с 10:00 до 12:30 или с 12:30 до 13:00. В разрабатываемой программе промежуток времени представлен в виде <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D1%80%D1%82%D0%B5%D0%B6_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)">кортежей</a> из двух целых чисел. Число означает номер 30-минутного блока, который идет после 9:00 утра. Например, кортеж (2, 4) означает диапазон с 10:00 до 11:00, а (0, 1) — это промежуток 9:00-9:30.</p><p>Вам нужно написать функцию, которая должна упростить вывод информации таким образом, что если команда занята в промежутках с 10:00 до 12:30 и с 12:30 до 13:00, то это отображалось как 10:00‒13:00. Например: на входе вашей функции неупорядоченный массив из кортежей [(0, 1), (3, 5), (4, 8), (10, 12), (9, 10)], а на выходе вы должны получить упорядоченный массив [(0, 1), (3, 8), (9, 12)].</p><p>В будущем планируется внести изменения в программу, где вместо 30-минутных блоков будут минутные, как это реализовано в представлении <a href="https://ru.wikipedia.org/wiki/UNIX-%D0%B2%D1%80%D0%B5%D0%BC%D1%8F">Unix-времени</a>. С учетом этого изменения нужно, чтобы ваша функция уже сейчас могла работать с большими числами. Еще не забудьте, что кортеж — это такой тип данных, в котором содержимое переменной невозможно изменять после ее создания.</p><p>Ответ</p><p>Задача, которую давали на собеседованиях в Apple. Представьте, что вы получили работу кассира в магазине. Ваш босс случайно выяснил, что вы обладаете навыками программиста, и захотел, чтобы вы помогли ему написать программу.</p><p>Входные данные:</p><ol><li>Указанная сумма денег.</li><li>Массив со всеми доступными номиналами монет.</li></ol><p>Нужно написать функцию, которая на выходе выдаст количество всех возможных способов получить указанную сумму денег при помощи различных доступных номиналов монет. Например, если вам нужно получить 4 цента из монет номиналами 1, 2 и 3 цента, то функция вернет 4 — именно столько есть возможных комбинаций из чисел 1, 2 и 3, чтобы получить в сумме 4:</p><ol><li>1, 1, 1, 1.</li><li>1, 1, 2.</li><li>1, 3.</li><li>2, 2.</li></ol><p>Ответ</p><p>Эту задачу когда-то давали в Google.</p><p>Вам нужно подняться по лестнице. За один раз можно подняться на одну или две ступеньки. Сколько существует способов добраться до N-й ступеньки?</p><p>Ответ</p><p>Эту задачу <a href="https://web.archive.org/web/20160402060709/https://medium.com/@bearsandsharks/i-failed-a-twitter-interview-52062fbb534b">задавали</a> на собеседовании в Twitter.<br />Рассмотрим следующую картинку:</p><figure><img src="https://media.tproger.ru/uploads/2016/05/1-9BE6dgt1cTDOHZSON7mlzQ.jpeg" alt="" /></figure><p>На этой картинке изображены стены различной высоты в некотором плоском мире. Картинка представлена массивом целых чисел, где индекс — это точка на оси X, а значение каждого индекса — это высота стены (значение по оси Y). Картинке выше соответствует массив [2, 5, 1, 2, 3, 4, 7, 7, 6].</p><p>Теперь представьте, что начался дождь, который не прекращается и поливает стены сверху равномерным потоком. Сколько воды соберется в «лужах» между стенами?</p><figure><img src="https://media.tproger.ru/uploads/2016/05/1-MKQZbWOdLPK-DD10Y48fkQ.jpeg" alt="" /></figure><p>задача про стены и дождь, собеседование в Twitter</p><p>Единицей объема воды считаем квадратный блок 1×1. На картинке выше всё, что расположено слева от точки 1, выплескивается. Вода справа от точки 7 также прольется. У нас остается лужа между 1 и 6 — таким образом, получившийся объем воды равен 10.</p><p>Ответ</p><h2>Задачи на нестандартное мышление</h2><p>Задача про бесконечный поезд.</p><p>Представьте себе замкнутую по окружности железную дорогу. По ней едет поезд, последний вагон которого скреплён с первым так, что внутри можно свободно перемещаться между вагонами. Вы оказались в одном случайном вагоне и ваша задача — подсчитать их общее количество. В каждом вагоне можно включать или выключать свет, но начальное положение переключателей случайное и заранее неизвестно.</p><p>Все вагоны внутри выглядят строго одинаково, окна закрыты так, что невозможно посмотреть наружу, движение поезда равномерное. Помечать вагоны как-либо, кроме включения или выключения света, нельзя. Количество вагонов конечно (не верьте названию задачи).</p><p>Ответ</p><p>Вы стоите перед закрытой комнатой, в которой есть три лампочки. На стене перед вами три переключателя: каждый из которых включает или выключает одну из лампочек. Вам нужно узнать, какой переключатель к какой лампочке относится, при условии, что зайти в комнату вы можете только один раз.</p><p>Расположение переключателей случайное, порядок подключения заранее неизвестен. Зайдя в комнату, можно делать с лампочками всё, что угодно, но уже нельзя вернуться к переключателям. Изначально все лампы выключены.</p><p>Если вы хотите решить задачу самостоятельно, но в голову ничего не приходит, можете воспользоваться нашей подсказкой.</p><p>Или смотрите сразу ответ</p><p>Классическая задача. Найдите в данной вам строке максимальную по длине подстроку, которая является палиндромом (то есть читается слева направо и справа налево одинаково). Предложите как можно более эффективный алгоритм.</p><p>Ответ</p><p>Реализуйте функцию извлечения квадратного корня, не пользуясь встроенными в язык средствами нахождения корня и возведения в степень.</p><p>Ответ</p><p>В этой задаче вам необходимо реализовать функцию, которая бы проверяла число на четность, используя только битовые операции AND, OR, NOT.</p><p>Ответ</p><p>На прямой даны N отрезков (в реальной жизни это могут быть промежутки времени, например), которые заданы координатами их левого и правого конца. Для каждого данного отрезка необходимо узнать, сколько из данных отрезков полностью находятся в нем. Один отрезок полностью содержится во втором, если левый конец первого отрезка находится правее левого конца второго отрезка, а правый конец первого находится левее правого конца второго. Предложите как можно более эффективный способ решения этой задачи. Гарантируется, что все концы данных отрезков различны.</p><p>Если вы придумали решение, то написать и проверить его вы можете <a href="http://codeforces.com/contest/652/problem/D">здесь</a>, на codeforces.</p><p>Сможете ли вы решить эффективно данную задачу в случае, если концы отрезков могут совпадать?</p><p>Ответ</p><p>Придумать алгоритм, определяющий, все ли символы в строке встречаются один раз.<br />При выполнении этого задания нельзя использовать дополнительные структуры данных.<br />Ответ</p><p>В массиве случайных чисел A[0…n-1] задан один «волшебный» индекс: такой, что A[i] = i. Значения элементов в массиве повторяться не могут. Учитывая, что массив отсортирован по значениям в порядке возрастания, напишите метод, который определит этот «волшебный» индекс, если он существует в массиве A. Если элемента в массиве нет, верните любое отрицательное число.</p><p>Как изменится решение, если известно, что таких индексов в массиве несколько?</p><p>Ответ</p><p>Как можно узнать количество дней в месяце, зная его номер? Другими словами, опишите, как получить функцию f(x), которая бы давала следующий список значений:</p><p><a href="https://media.tproger.ru/uploads/2016/10/table1.png"></a></p><p>В качестве аргумента мы получаем только номер месяца, т.е. мы не учитываем високосные года, и f(2) = 28.</p><p>Ответ</p><p>Задачка для питонистов: нужно перебрать все пары символов в строке, и остановиться при нахождении двух одинаковых символов.</p><p>Решение достаточно очевидное, но возникает вопрос:</p><p>Если бы мы программировали, например, на Java, то мы могли бы воспользоваться механизмом меток:</p><p>Однако в Python такого механизма нет. Требуется предложить наиболее удобное в использовании и читаемое решение.</p><p>Ответ</p><p>Написать код, который проверяет, пересекутся ли две заданные прямые, лежащие в одной плоскости.<br />Предположим, что нам необходимо разработать структуру данных для хранения информации о прямой, и будем считать, что если две линии совпадают, то они пересекаются.</p><p>Ответ</p><p>Классическая задача: посчитать N-е число последовательности, в которой каждый элемент равен сумме двух предыдущих. Такая последовательность называется последовательностью Фибоначчи: 1, 1, 2, 3, 5, 8…</p><p>Очень часто на разнообразных олимпиадах попадаются задачи вроде этой, которые, как думается на первый взгляд, можно решить с помощью простого перебора. Но если мы подсчитаем количество возможных вариантов, то сразу убедимся в неэффективности такого подхода: например, простая рекурсивная функция, приведенная ниже, будет потреблять существенные ресурсы уже на 30-ом числе Фибоначчи, тогда как на олимпиадах время решения часто ограничено 1-5 секундами.</p><p>Придумайте, как найти N’е число Фибоначчи за приемлемое время.</p><p>Ответ</p><p>Чтобы разобраться в теме, рекомендуем почитать нашу <a href="https://tproger.ru/articles/dinprog-starters/">статью про динамическое программирование</a> для начинающих.</p><h2>Задачи на программирование</h2><p>Одна из самых известных задач Интернета, будоражащая многие светлые умы человечества.</p><p>Самолет стоит на взлетной полосе с подвижным покрытием типа транспортера. Покрытие может двигаться против направления взлета самолета, то есть ему навстречу. Транспортер автоматически регулирует свою скорость таким образом, чтобы самолет оставался неподвижным. Вопрос: сможет ли самолет в таких условиях взлететь?</p><p><a href="https://media.tproger.ru/uploads/2016/11/x_06ad251d.jpg"></a></p><p>Ответ</p><p>Задача на перегрузку функций в C++, которая может оказаться сложнее, чем выглядит.</p><p>Предположим, у нас есть два класса:</p><p>Что выведут два следующих куска кода и почему?</p><p>Ответ</p><p>Рассмотрим ситуацию, когда три работника хотят вычислить свою среднюю зарплату при условии, что каждый знает свою зарплату, но не может сообщить ее другому напрямую. Обмен информации между людьми возможен, но передаваемые друг другу сообщения не должны содержать какую-либо конкретную информацию об уровне зарплат. Как это сделать?</p><p>Ответ</p><p>Пусть у нас есть массив положительных чисел, в котором все числа, кроме трех, встречаются по 2 раза, а эти три числа отличны от всех остальных и встречается каждое ровно по одному разу. Нужно найти эти три числа. Числа помещаются в 32-битный целочисленный тип.</p><p>Ответ</p><p>Положим, у нас есть некоторая конечная последовательность чисел и мы имеем итератор, указывающий на первый элемент. Мы можем при помощи итератора посмотреть значение текущего элемента и перейти к следующему элементу. Требуется построить такой алгоритм выбора случайного элемента из этой последовательности, чтобы каждый элемент мог оказаться выбранным с равной вероятностью.</p><p>Ограничения: мы можем использовать O(1) дополнительной памяти и не можем создавать новый итератор. Можно пользоваться функцией генерации случайного числа от [0;1).</p><p>Ответ</p><p>Известная задача с IT-собеседований с несколькими вариантами решения: как правильно реализовать обмен значений переменных?</p><p>Ответ</p><p>На одной стороне реки находятся три человека и три льва. Все они должны оказаться на другом берегу реки. Есть лишь одна лодка, в которой могут поместиться лишь два живых существа одновременно (человека или льва). Вы не можете оставлять на том или другом берегу реки больше львов, чем людей, так как в этом случае животные съедят людей, оставшихся в меньшинстве. Как вы переправите всех через реку?</p><p>Ответ</p><p>Если бы вы получили стопку монет достоинством в один пенс каждая и высотой с Эмпайр-стейт-билдинг, поместились бы все эти деньги в одном помещении?</p><p>Сперва может показаться, что это одна из тех головоломок, в которых предполагается оценить какое-то абсурдное число. Но на самом деле это не так, подумайте хорошенько.</p><p>Ответ</p><h2>Часто задаваемые вопросы</h2><h3>Какие задачи дают на IT-собеседованиях?</h3><p>Чаще всего дают алгоритмические задачи, задачи на структуры данных и логические головоломки.</p><h3>Как подготовиться к интервью?</h3><p>Повторите алгоритмы, решайте задачи на LeetCode, практикуйтесь объяснять решение вслух.</p><h3>На каком языке решать задачи?</h3><p>Python популярен благодаря краткому синтаксису. Java и C++ тоже часто используются.</p><p>Также рекомендуем по теме:</p><ul><li><a href="https://tproger.ru/digest/competitive-programming-practice/">28 cайтов с задачами по программированию</a></li><li><a href="https://tproger.ru/translations/top-ten-interview-preparation-mistakes/">Десять наиболее частых ошибок программистов при подготовке к собеседованию</a></li><li><a href="https://tproger.ru/problems/string-algorithms/">Подборка полезных алгоритмов для собеседований: задачи на строки</a></li><li><a href="https://tproger.ru/problems/technocup-problems-2017/">14 алгоритмических задач с разбором решений по итогам «Технокубка» 2017</a></li><li><a href="https://tproger.ru/problems/technocup-final-solutions/">Олимпиада «Технокубок»: разбор задач финального раунда</a></li><li><a href="https://tproger.ru/problems/russian-code-cup-2017/">Разбираем решения задач предварительного тура Russian Code Cup</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>Подсчёт количества путей робота на сетке</title>
      <link>https://tproger.ru/problems/grid-robot</link>
      <comments>https://tproger.ru/problems/grid-robot?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/problems/grid-robot</guid>
      <description><![CDATA[<p>Робот идёт из точки (0, 0) в точку (X, Y) только вправо и вниз: как посчитать количество маршрутов и учесть непроходимые области сетки.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/problems/grid-robot">Подсчёт количества путей робота на сетке</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Задачи умеренной сложности]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Задачки]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 03 Sep 2015 22:30:38 GMT</pubDate>
      <content:encoded><![CDATA[<p>Представьте себе робота, находящегося в левом верхнем углу сетки с координатами (X, Y). Робот может перемещаться в двух направлениях: вправо и вниз. Сколько существует маршрутов, проходящих от точки (0, 0) до точки (X, Y)?</p><h3>Дополнительно:</h3><p>Предположите, что на сетке существуют области, которые робот не может пересекать. Разработайте алгоритм построения маршрута от левого верхнего до правого нижнего угла.</p><h3>Решение</h3><p>Нам нужно подсчитать количество вариантов прохождения дистанции с Х шагов вправо и Y шагов вниз (X + Y шагов).</p><p>Чтобы создать путь, мы делаем Х шагов вправо так, чтобы общее количество перемещений оставалось фиксированным (X + Y). Таким образом, количество путей должно совпадать с количеством способов выбрать Х элементов из X + Y, то есть биномиальным коэффициентом. Биномиальный коэффициент из n по r имеет вид:</p><p>Для нашей задачи выражение будет следующим:</p><p>Даже если вы незнакомы с комбинаторикой, то все равно можете найти решение этой задачи самостоятельно.</p><p>Представим путь как строку длиной X + Y, состоящую из X символов R и Y символов D. Мы знаем, что из X + Y неповторяющихся символов мы можем составить (X + Y)! строк. Но в нашем случае используется X символов R и Y символов D. Символы R могут быть расставлены X! способами (то же самое мы можем сделать и с символами D). Таким образом, необходимо убрать лишние строки X! и Y!. В итоге мы получим то же самое выражение:</p><h3>Дополнительно</h3><p>Найдите маршрут (на карте есть места, через которые не может пройти робот).</p><p>Если мы изобразим нашу карту, то единственный способ попасть в квадрат (X, Y) — оказаться в одном из смежных квадратов: (X-1, Y) или (X, Y-1). Следовательно, необходимо найти путь к любому из этих квадратов ((X-1, Y) или (X, Y-1)).</p><p>Как это осуществить? Чтобы найти путь в квадрат (X-1, Y) или (X, Y-1), мы должны оказаться в одной из смежных ячеек. То есть нам необходимо найти путь к квадрату, смежному с (X-1, Y) ((X-2, Y) и (X-1, Y-1)) или (X, Y-1) ((X-1, Y-1) и (X, Y-2)). Обратите внимание: в наших рассуждениях точка (X-1, Y-1) упоминается дважды, мы еще вернемся к этому факту.</p><p>Давайте попробуем найти путь от исходного квадрата, двигаясь в обратном направлении, — начинаем с последней ячейки и пытаемся найти путь к каждому смежному квадрату. Далее приведен рекурсивный код, реализующий наш алгоритм.</p><p>Помните, что маршруты дублируются? Чтобы найти все пути к (X, Y), мы находим все пути к (X-1, Y) и (X, Y-1). Затем мы смотрим на координаты смежных квадратов: (X-2, Y), (X-1, Y-1), (X-1, Y-1) и (X, Y-2). Квадрат (X-1, Y-1) появляется дважды. Давайте будем запоминать посещенные квадраты, чтобы не тратить на них время.</p><p>Это можно сделать с помощью следующего алгоритма динамического программирования:</p><p>Это простое изменение сделает наш код более быстрым.</p><p>Разбор задачи по книге «Карьера программиста. Как устроиться на работу в Google, Microsoft или другую ведущую IT-компанию»</p>]]></content:encoded>
    </item>
    <item>
      <title>У вас есть неограниченное количество монет достоинством 25, 10, 5 и 1 цент. Напишите код, определяющий количество способов представления n центов</title>
      <link>https://tproger.ru/problems/coins</link>
      <comments>https://tproger.ru/problems/coins?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/problems/coins</guid>
      <description><![CDATA[<p>Рекурсивная задача о размене: количество способов набрать нужную сумму монетами разного достоинства выводится через решения подзадач.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/problems/coins">У вас есть неограниченное количество монет достоинством 25, 10, 5 и 1 цент. Напишите код, определяющий количество способов представления n центов</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Задачи умеренной сложности]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Задачки]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 04 Aug 2015 16:42:41 GMT</pubDate>
      <content:encoded><![CDATA[<p>Это рекурсивная задача, поэтому давайте разберемся, как рассчитать makeChange(n), основываясь на предыдущих решениях (подзадачах). Пусть n = 100. Мы хотим вычислить количество способов представления 100 центов.</p><p>Нам известно, что для получения 100 центов мы можем использовать монеты 0, 1, 2, 3 или 4 четвертака (25 центов):</p><p>Двигаемся дальше: попробуем упростить некоторые из этих задач. Например, makeChange(100, используя 1 четвертак) = makeChange(75, используя 0 четвертаков). Это так, потому что если мы должны использовать один четвертак для представления 100 центов, оставшиеся варианты соответствуют различным представлениям 75 центов.</p><p>Мы можем применить эту же логику для makeChange(100, используя 2 четвертака), makeChange(100, используя 3 четвертака) и makeChange(100, используя 4 четвертака).<br />Приведенное ранее выражение можно свести к следующему:</p><p>Заметьте, что последнее выражение — makeChange(100, используя 4 четвертака) — равно 1.</p><p>Что делать дальше? Теперь мы израсходовали все четвертаки и можем использовать следующую самую крупную монету — 10 центов.</p><p>Подход, использованный для четвертаков, подойдет и для 10–центовых монет. Мы применим его для четырех частей приведенного выше выражения. Так, для первой части:</p><p>После этого можно перейти к монеткам в 5 и 1 цент. В результате мы получим древовидную рекурсивную структуру, где каждый вызов расширяется до четырех или больше вызовов.</p><p>Базовый случай для нашей рекурсии — полностью сведенное (упрощенное) выражение. Например, makeChange(50, используя 0 четвертаков, 5 монет в 10 центов) полностью сводится к 1, так как 5 монет по 10 центов дает ровно 50 центов.</p><p>Рекурсивный алгоритм будет иметь примерно такой вид:</p><p>Хотя мы реализовали код, опираясь на монеты, используемые в США, его можно легко адаптировать для любой другой валюты.</p><p>Разбор задачи по книге «Карьера программиста. Как устроиться на работу в Google, Microsoft или другую ведущую IT-компанию»</p>]]></content:encoded>
    </item>
    <item>
      <title>Найдите самую большую сумму непрерывной последовательности из массива целых чисел, как положительных, так и отрицательных</title>
      <link>https://tproger.ru/problems/continuous-sequence-with-greatest-sum</link>
      <comments>https://tproger.ru/problems/continuous-sequence-with-greatest-sum?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/problems/continuous-sequence-with-greatest-sum</guid>
      <description><![CDATA[<p>Массив содержит и положительные, и отрицательные числа: разбор популярной задачи на примере последовательности 2 3 -8 -1 2 4 -2 3.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/problems/continuous-sequence-with-greatest-sum">Найдите самую большую сумму непрерывной последовательности из массива целых чисел, как положительных, так и отрицательных</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Задачи умеренной сложности]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Задачки]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 02 Jul 2015 10:43:39 GMT</pubDate>
      <content:encoded><![CDATA[<p>Это довольно сложная, но очень популярная задача. Давайте решим ее на примере массива:</p><p>2 3 -8 -1 2 4 -2 3</p><p>Если рассматривать массив как содержащий чередующиеся последовательности положительных и отрицательных чисел, то не имеет смысла рассматривать части положительных или отрицательных субпоследовательностей. Почему? Включая часть отрицательной субпоследовательности, мы уменьшаем итоговое значение суммы, значит, нам не стоит включать часть отрицательной субпоследовательности вообще. Включение части положительной субпоследовательности выглядит еще более странным, поскольку включение этой субпоследовательности целиком всегда даст больший результат.</p><p>Нужно придумать алгоритм, рассматривая массив как последовательность отрицательных и положительных чисел, расположенных вперемежку.</p><p>Любое число можно представить в виде суммы субпоследовательностей положительных и отрицательных чисел. В нашем примере массив можно сократить до:</p><p>5 -9 6 -2 3</p><p>Мы еще не получили отличный алгоритм, но теперь лучше понимаем, с чем имеем дело.</p><p>Рассмотрим предыдущий массив. Нужно ли учитывать субпоследовательность {5, -9}? В сумме мы получим -4, значит, нет смысла учитывать оба этих числа, достаточно только {5}.</p><p>В каких случаях имеет смысл учитывать отрицательные числа? Только если это позволяет нам объединить две положительные субпоследовательности, сумма каждой из которых больше, чем вклад отрицательной величины.</p><p>Давайте продвигаться, начиная с первого элемента в массиве.</p><p>5 — это самая большая сумма, встретившаяся нам. Таким образом, maxsum = 5 и sum = 5. Затем мы видим следующее число (-9). Если добавить это число к sum, то получится отрицательная величина. Нет смысла расширять субпоследовательность с 5 до -9 (-9 уменьшает общую сумму до 4). Таким образом, мы просто сбрасываем значение sum.</p><p>Теперь мы дошли до следующего элемента (6). Эта субпоследовательность больше, чем 5, таким образом, мы обновляем значения maxsum и sum.</p><p>Затем мы смотрим на следующий элемент (-2). Добавление этого числа к 6 сделает  sum = 4. Так как это не окончательное значение, наша субпослсдовательность выглядит как {6, -2}. Мы обновляем sum, но не maxsum.</p><p>Наконец мы смотрим па следующий элемент (3). Добавление 3 к sum (4) даст нам 7, таким образом, мы обновляем maxsum. Максимальная последовательность имеет вид {6, -2, 3}.</p><p>Когда мы работаем с развернутым массивом, логика остается такой же. Следующий код реализует этот алгоритм:</p><p>А если массив состоит из отрицательных чисел? Как действовать в этом случае? Рассмотрим простой массив {-3, -10, -5}. Можно дать три разных ответа:</p><p>• -3 (если считать, что субпоследовательность не может быть пустой);<br />• 0 (субпоследовательность может иметь нулевую длину);<br />• MINIMUM_INT (для случая ошибки).</p><p>В нашем коде был использован второй ответ (sum = 0), но в этом вопросе не существует однозначного «правильного» решения. Обсудите это с интервьюером.</p><p>Разбор задачи по книге «Карьера программиста. Как устроиться на работу в Google, Microsoft или другую ведущую IT-компанию»</p>]]></content:encoded>
    </item>
    <item>
      <title>Золотая пирамида — задача про треугольник, составленный из чисел</title>
      <link>https://tproger.ru/problems/golden-pyramid</link>
      <comments>https://tproger.ru/problems/golden-pyramid?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/problems/golden-pyramid</guid>
      <description><![CDATA[<p>Классическая задача «Золотая гора» с CheckiO: спуск по треугольнику из чисел и поиск маршрута с максимальной суммой, включая решение рекурсией.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/problems/golden-pyramid">Золотая пирамида — задача про треугольник, составленный из чисел</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Задачи умеренной сложности]]></category>
      <category><![CDATA[Рекурсия и динамика]]></category>
      <category><![CDATA[Задачки]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 06 May 2015 21:45:45 GMT</pubDate>
      <content:encoded><![CDATA[<p>В этом выпуске рассмотрим классическую задачу, известную под названием «Золотая гора». На CheckiO её реализовали в <a href="http://www.checkio.org/mission/golden-pyramid/share/b88523a147fdb0960da155eb777729f0/">этой задаче</a>.</p><p>Представьте себе треугольник, составленный из чисел. Одно число расположено в вершине. Ниже размещено два числа, затем три, и так до нижней грани. Вы начинаете на вершине, и нужно спуститься к основанию треугольника. За каждый ход вы можете спуститься на один уровень и выбрать между двумя числами под текущей позицией. По ходу движения вы «собираете» и суммируете числа, которые проходите. Ваша цель — найти максимальную сумму, которую можно получить из различных маршрутов.</p><p>Рассмотрим различные методы решения.</p><h3>Рекурсия</h3><p>Первым делом в голову приходит мысль использовать рекурсию и просчитать все пути от вершины. Когда мы спускаемся на один уровень, то все доступные числа ниже образуют новый меньший треугольник, и можно запустить нашу функцию уже для нового подмножества и так пока не достигнем основания.</p><p>Как мы видим, на первом уровне мы запустим нашу функцию два раза, затем 4, 8, 16 раз и так далее. В итоге мы получим сложность алгоритма 2N и, например, для 100-уровневой пирамиды нам нужно будет уже где-то ≈1030 вызовов функции. Многовато.</p><h3>Динамическое программирование</h3><p>Что если попробовать использовать принцип динамического программирования и разбить нашу проблему на множество мелких подзадач, результаты которых мы затем аккумулируем. Попробуйте взглянуть на треугольник вверх ногами. А теперь на второй уровень (то есть предпоследний от основания). Для каждой ячейки мы можем решить, каким будет лучший выбор в наших маленьких трёхэлементных треугольничках. Выбираем лучший, суммируем с рассматриваемой ячейкой и записываем результат. Таким образом, мы получили наш треугольник, но на один уровень ниже. Повторяем данную операцию снова и снова. В результате нам нужно (N-1)+(N-2)+…2+1 операций и сложность алгоритма равна N2.</p><h3>Решения игроков CheckiO</h3><p>Пользователь gyahun_dash написал интересную реализацию описанного выше метода ДП в своем решении <a href="http://www.checkio.org/mission/golden-pyramid/publications/gyahun_dash/python-3/dp/share/28008da26f7ecba0593f7b71a5250b25/">«DP»</a>. Он использовал reduce, чтобы проходить по парам строк, и map чтобы обработать каждую из них.</p><p>Игрок evoynov использовал двоичные числа, чтобы перебрать все возможные маршруты, представленные как последовательность 1 и 0 в своем решении <a href="http://www.checkio.org/mission/golden-pyramid/publications/evoynov/python-3/binaries/share/95c5578eef9be0c793fc37fe54bdc95e/">«Binaries»</a>. И это наглядный пример сложности алгоритма с рекурсией и перебором всех маршрутов.</p><p>И чтобы не было скучно, посмотрим на легкий мозгодробитель от пользователя nickie и его однострочник <a href="http://www.checkio.org/mission/golden-pyramid/publications/nickie/python-3/functional-dp/share/98bff2a8ad1f0ca4897de6e884ec384d/">«Functional DP»</a>, который только формально состоит из двух строк. Конечно, это решение из категории «Творческих» («Creative»). Не думаю, что автор использует такое на боевом коде. А просто для так для веселья, почему бы и нет.</p><p>Вот и всё на сегодня. Делитесь вашими идеями и мыслями.</p><p>Спасибо CheckiO за интересную задачу.</p><p><a href="http://www.checkio.org/"></a></p>]]></content:encoded>
    </item>
  </channel>
</rss>