Паттерны задач: как выбрать подход
Название техники — итог рассуждения. Сначала определите форму ответа, свойства данных и работу, которую прямое решение повторяет.
Пять вопросов перед выбором паттерна
- Что ищем? Пару, непрерывный фрагмент, путь, количество способов или лучший вариант? Подмассив сохраняет непрерывность, подпоследовательность может пропускать элементы.
- Какие свойства гарантированы? Порядок, знак чисел, модель весов, возможность менять вход. Слово «массив» само по себе не выбирает алгоритм.
- Что делает перебор? Назовите кандидатов и повторную работу: поиск в префиксе, пересчёт диапазона или одинаковую остаточную подзадачу.
- Какое состояние достаточно? Границы, частоты, visited, минимум в куче или ответ DP. Запишите инвариант до реализации.
- Что опровергает идею? Постройте маленький контрпример и оцените время и память через Big O до усложнения кода.
Техники сочетаются: префиксная сумма даёт значение диапазона, а хеш-таблица хранит частоты уже встреченных префиксов. Поэтому ниже ориентиры для проверки гипотез, а не правило «одно ключевое слово — один шаблон».
Два указателя
Признак задачи: Можно безопасно сдвигать одну из границ состояния.
- Когда подходит
- Данные упорядочены, а сравнение позволяет доказать, что одна граница больше не участвует в ответе. Сначала сформулируйте, какие кандидаты сохраняются после каждого сдвига.
- Когда не подходит
- На произвольном массиве увеличение левого значения не обязано увеличивать сумму. Сортировка тоже не всегда допустима: она меняет порядок и требует сохранить индексы, если ответу нужны исходные позиции.
- Проверьте на примере
- Для пары с заданной суммой в отсортированном массиве слишком малая сумма исключает текущий левый элемент. В несортированном массиве такое исключение неверно; рассмотрите множество или сортировку с учётом контракта.
- Стоимость
- Если каждый указатель движется только вперёд или внутрь диапазона, суммарное время O(n), память O(1).
Скользящее окно
Признак задачи: Ответ относится к непрерывному фрагменту с ремонтируемым инвариантом.
- Когда подходит
- Ищется непрерывный фрагмент, состояние обновляется при добавлении справа и удалении слева, а нарушение можно исправить движением левой границы без потери ответа.
- Когда не подходит
- Одного слова «подмассив» недостаточно. Для суммы с отрицательными числами стандартное сжатие окна по слишком большой сумме теряет монотонность. Проверяйте именно условие допустимости.
- Проверьте на примере
- В строке без повторов новый дубликат указывает, какую левую часть удалить. Для подсчёта подмассивов с точной суммой и произвольными знаками полезнее рассмотреть префиксные суммы вместе с хешированием.
- Стоимость
- O(n) движений границ. Для окна уникальности с хеш-таблицей ожидаемое время O(n), память O(u), где u — число разных просмотренных символов; обновление состояния должно быть дешёвым.
Префиксная сумма
Признак задачи: Нужны суммы или счётчики многих диапазонов.
- Когда подходит
- Много запросов суммы или количества на неизменяемых диапазонах. Одна подготовка позволяет получать ответ как разность двух префиксов, включая пустой префикс.
- Когда не подходит
- Частые изменения элементов делают обычные префиксы дорогими: приходится пересчитывать последующие значения. Минимум диапазона нельзя получить простой разностью минимумов префиксов.
- Проверьте на примере
- Для суммы на [l, r) вычислите prefix[r] − prefix[l]. Зафиксируйте полуинтервальные границы до кода и проверьте l = 0, пустой диапазон и r = n.
- Стоимость
- Построение O(n) времени и O(n) памяти; запрос суммы или счётчика полуинтервала после подготовки — O(1).
Хеширование
Признак задачи: Повторяется проверка наличия, частоты или соответствия.
- Когда подходит
- Повторяется вопрос «видели ли раньше?», нужен счётчик частот или соответствие ключа значению. Сохраняйте именно информацию, которая нужна следующим шагам, а не всю историю поиска.
- Когда не подходит
- Хеш-таблица не поддерживает упорядоченные соседства и сама по себе не даёт гарантированного O(1) в худшем случае. Для длинных ключей нужно учитывать стоимость хеширования и сравнения.
- Проверьте на примере
- Проверяя дубликаты, сначала спросите, есть ли значение в seen, и только затем вставьте его. Если вставить раньше, каждый элемент ошибочно станет собственным повтором.
- Стоимость
- Для ключей постоянного размера поиск и удаление ожидаемо O(1), вставка ожидаемо амортизированно O(1). Отдельная операция в худшем случае O(n) из-за коллизий или расширения; память O(n).
Бинарный поиск
Признак задачи: Есть монотонный предикат или упорядоченная граница.
- Когда подходит
- Есть отсортированный индексируемый диапазон или предикат с единственной границей false → true. Проверка середины должна обоснованно исключать половину пространства.
- Когда не подходит
- Если предикат меняет значение туда и обратно, граница не определена. Поиск по ответу требует отдельно учесть цену проверки; доступ к середине связного списка тоже не постоянный.
- Проверьте на примере
- Для первого элемента не меньше target подходящий mid остаётся кандидатом, поэтому продолжаем искать левее. Результат n означает позицию вставки за последним элементом, а не ошибку алгоритма.
- Стоимость
- Поиск по индексируемому упорядоченному пространству: O(log n) времени и O(1) памяти в итеративной форме.
Стек
Признак задачи: Нужно обработать последнюю незавершённую сущность.
- Когда подходит
- Следующий шаг должен обработать последнюю незавершённую сущность: открытую скобку, вложенный контекст или отложенного кандидата. Это порядок LIFO.
- Когда не подходит
- Если первым должен обрабатываться самый ранний обнаруженный элемент, нужна очередь. Для ближайшего большего значения потребуется ещё и доказанный монотонный порядок кандидатов, а не просто стек.
- Проверьте на примере
- Закрывающая скобка должна соответствовать вершине стека. Проверяйте пустоту до чтения, тип пары при удалении и отсутствие незакрытых скобок в конце.
- Стоимость
- Чтение вершины и pop с конца — O(1); push в стек поверх динамического массива амортизированно O(1), отдельное расширение — O(n); память O(n).
Куча
Признак задачи: Нужно многократно получать текущий минимум или максимум.
- Когда подходит
- Нужно многократно извлекать текущий минимум или максимум, а кандидаты поступают или меняются между извлечениями. Для Top K можно удерживать лишь ограниченный набор лучших.
- Когда не подходит
- Если нужен полный порядок одного неизменного набора, сортировка может быть проще. Куча не ускоряет произвольный поиск и не делает любой соседний элемент массива следующим по величине.
- Проверьте на примере
- Минимум находится в корне, но брать второй элемент массива как второй минимум нельзя. После извлечения восстановите инвариант. В C++ priority_queue по умолчанию max-heap, обычные операции Python heapq используют min-heap.
- Стоимость
- В двоичной куче вершина O(1), просеивание O(log n), построение O(n), память O(n). Для кучи на растущем массиве вставка амортизированно O(log n), отдельное расширение может стоить O(n).
BFS — поиск в ширину
Признак задачи: Сущности соединены произвольными отношениями или переходами.
- Когда подходит
- Нужен путь с минимальным числом рёбер в невзвешенном графе или слоями распространяется состояние. Все источники multi-source BFS начинаются на расстоянии 0.
- Когда не подходит
- Обычная очередь BFS не учитывает разные веса рёбер. Для неотрицательных разных весов изучите Дейкстру; отрицательные веса требуют другого алгоритма и отдельного анализа.
- Проверьте на примере
- Помечайте вершину при добавлении в очередь: тогда несколько соседей не добавят её повторно. Первый слой — источники, каждый следующий добавляет одно ребро к расстоянию.
- Стоимость
- Для списков смежности полный обход O(V + E) времени и O(V) дополнительной памяти. Хранение самого графа O(V + E) считается отдельно.
DFS — поиск в глубину
Признак задачи: Сущности соединены произвольными отношениями или переходами.
- Когда подходит
- Нужно исследовать достижимость, компоненты или структуру переходов, продолжая одну ветвь до возврата. В несвязном графе внешний цикл запускает обход из каждой ещё не посещённой вершины.
- Когда не подходит
- Первый найденный DFS путь не обязательно кратчайший. Проверка циклов зависит от ориентированности графа; правило пропуска родителя нельзя бездумно переносить на ориентированный граф.
- Проверьте на примере
- Visited устанавливается до переходов, иначе цикл приведёт к повторным вызовам. На длинной цепочке глубина рекурсии растёт до V: в Python учитывайте предел рекурсии, при необходимости используйте явный стек.
- Стоимость
- Для списков смежности O(V + E) времени и O(V) дополнительной памяти с visited и стеком; память представления графа учитывается отдельно.
Жадный выбор
Признак задачи: Локальный выбор можно доказуемо включить в оптимальное решение.
- Когда подходит
- Локальный выбор можно доказуемо включить в оптимальное решение. Попробуйте обменный аргумент: заменить первый выбор оптимального решения своим так, чтобы ответ не ухудшился.
- Когда не подходит
- «Возьму самый большой» — гипотеза, а не доказательство. Для монет произвольных номиналов жадный выбор может быть неверным, хотя работает на некоторых наборах.
- Проверьте на примере
- Для монет 1, 3, 4 и суммы 6 выбор 4 даёт три монеты (4 + 1 + 1), а 3 + 3 — две. Если выбор нельзя доказать, вернитесь к полному перебору и ищите состояние DP.
- Стоимость
- Цена зависит от выбора кандидата; типичная схема с предварительной сортировкой занимает O(n log n) времени.
Бэктрекинг
Признак задачи: Нужно исследовать выборы, отменяя изменения состояния.
- Когда подходит
- Нужно построить варианты последовательностью выборов, ограничения отсекают часть ветвей, а изменения состояния можно отменить перед соседней ветвью.
- Когда не подходит
- При больших размерах полный перебор остаётся непригодным даже с несколькими отсечениями. Если важен лишь оптимальный ответ и повторяются одинаковые остаточные задачи, проверьте возможность мемоизации.
- Проверьте на примере
- Строя комбинацию, добавьте выбранный элемент, исследуйте продолжение и удалите его перед следующим выбором. Без отката состояние соседних ветвей смешается.
- Стоимость
- Грубая верхняя оценка числа узлов O(b^d) для b > 1 и глубины d; добавьте стоимость обработки узла и выдачи ответов. Стек и текущий путь обычно O(d), сохранённые ответы считаются отдельно.
Динамическое программирование
Признак задачи: Подзадачи перекрываются, а ответ определяется малым состоянием.
- Когда подходит
- Разные пути перебора приходят к одной остаточной задаче, а её ответ определяется компактным состоянием. Сформулируйте вопрос ячейки, базы, переходы и порядок зависимостей.
- Когда не подходит
- Если ключ пропускает важную часть истории, объединять подзадачи нельзя. Если состояний слишком много или они не повторяются, таблица не решит проблему сложности автоматически.
- Проверьте на примере
- Для лестницы с шагами 1 и 2 остаток r задаёт вопрос «сколько способов завершить путь?». База r = 0 даёт один пустой способ; одинаковые остатки нужно вычислять один раз.
- Стоимость
- Время обычно равно числу достижимых состояний, умноженному на число переходов; память — числу хранимых состояний.
Как тренировать распознавание
Сначала решите задачу с известной техникой. Затем закройте название и сформулируйте свойства задачи своими словами. Сравните две гипотезы, найдите контрпример к слабой и объясните инвариант сильной. Если вы узнали знакомый сюжет, это ещё не проверка нового условия.
Используйте практику с разбором, переносом и самостоятельным режимом, а затем LeetCode 75, сопоставленный урокам. После решения записывайте, какой признак был решающим и на каком изменении условия алгоритм перестаёт работать.
Для первого прохода следуйте порядку курса. Для подготовки к интервью откройте маршрут подготовки и повторения. Глубокий разбор выбора техники есть в уроке «Диагностика задачи без названия паттерна».