Деревья, кучи и базовые алгоритмы на графах
В предыдущих статьях курса мы учились ускорять решения за счёт правильных структур данных и паттернов: хеш-таблицы, префиксные суммы, два указателя, стек/очередь/deque, монотонные структуры, двоичный поиск по ответу. Следующий большой пласт задач алгоритмической секции — деревья и графы.
Дерево — частный случай графа, а куча — структура данных, которая чаще всего нужна для эффективного выбора лучшего кандидата на каждом шаге (например, в кратчайших путях). Эта статья даст базовый набор, который закрывает большинство графовых задач уровня интервью:
представление графа в памяти
обходы DFS и BFS
поиск компонент связности
проверка циклов и базовые свойства
топологическая сортировка в DAG
куча и Дейкстра для кратчайших путей с неотрицательными весами!Сравнение дерева, графа и кучи и как они выглядят
Базовые определения: граф, дерево, ориентированность
Граф состоит из:
множества вершин (узлов)
множества рёбер (связей)Граф бывает:
неориентированный: ребро связывает две вершины в обе стороны
ориентированный: ребро идёт из в и направление важно
взвешенный: у ребра есть вес (стоимость, длина)Дерево — это связный неориентированный граф без циклов. Важное свойство дерева:
если в дереве вершин, то рёбер ровно Обходы графа почти всегда требуют структуры из прошлой статьи:
DFS естественно реализуется стеком (часто рекурсией, которая по сути тоже стек)
BFS реализуется очередьюСправочные источники:
Граф (математика) — Википедия)
Дерево (теория графов) — Википедия)Как хранить граф: список смежности против матрицы
На секции почти всегда выбирают список смежности.
Список смежности
Идея:
для каждой вершины храним список соседейПамять:
, где — число вершин, — число рёберПлюсы:
эффективен для разреженных графов
удобно запускать BFS/DFSМатрица смежности
Идея:
храним таблицу , где ячейка показывает, есть ли реброПамять:
-
Минус:
при порядка это невозможно по памятиПрактическое правило:
если граф большой, почти наверняка нужен список смежности| Представление | Память | Проверка ребра | Перебор соседей вершины |
|---|---:|---:|---:|
| Список смежности | | обычно | |
| Матрица смежности | | | |
DFS: поиск в глубину
DFS (depth-first search) — обход, который идёт вглубь, пока может.
Типовые применения:
проверить достижимость (есть ли путь)
найти компоненты связности
обнаружить цикл
сделать топологическую сортировку (в ориентированном ациклическом графе)Инвариант DFS
вершина помечается как посещённая, и после этого мы больше не обходим её повторноИтеративный DFS (стеком)
Итеративная версия часто надёжнее рекурсии на больших входах (например, в Python из-за ограничений глубины рекурсии).
Справочник:
Depth First Search — CP-AlgorithmsBFS: поиск в ширину и кратчайшие пути в невзвешенном графе
BFS (breadth-first search) — обход слоями: сначала все вершины на расстоянии 1 ребро, потом 2 ребра и так далее.
Ключевой факт для задач:
в невзвешенном графе BFS даёт кратчайшее расстояние по числу рёберИнвариант BFS
когда вершина впервые извлекается из очереди, найдено её минимальное расстояние (в рёбрах) от стартаКод BFS для расстояний
Где в коде зашита математика:
dist[start] = 0 означает расстояние от старта до себя равно 0
dist[to] = dist[v] + 1 означает: мы пришли в to по одному ребру из v, поэтому расстояние увеличилось на 1Справочник:
Breadth First Search — CP-Algorithms!BFS идёт слоями и даёт кратчайшие расстояния по числу рёбер
Компоненты связности в неориентированном графе
Частая формулировка:
«сколько компонент связности?»
«разбейте вершины на группы достижимости»Решение:
идём по всем вершинам
если вершина не посещена, запускаем DFS/BFS и помечаем всю её компонентуСложность:
, потому что каждая вершина посещается максимум один раз, и каждое ребро рассматривается ограниченное число разДеревья как частный случай графов
В дереве нет циклов, и это упрощает многие вещи.
Корневое дерево и родитель
Часто дерево «укореняют» в вершине root. Тогда:
у каждой вершины (кроме корня) есть родитель
можно считать глубины (depth) и поддеревьяBFS/DFS на дереве
Они работают так же, как на графе, но для дерева важная оптимизация мыслительная:
если мы пришли из parent в v, то обратно по ребру к parent идти не надоВ коде это часто выглядит так:
либо используем visited
либо передаём parent и пропускаем егоВажно: пример выше показывает идею, но рекурсивная версия может упасть по глубине на длинной цепочке.
Куча (priority queue): быстрый доступ к минимуму/максимуму
Куча — структура данных для операций:
вставить элемент
достать элемент с минимальным (или максимальным) ключомТипичная модель сложности:
вставка:
извлечение минимума:
посмотреть минимум: Почему появляется логарифм:
куча поддерживает частичный порядок, и на восстановление свойств после вставки/удаления нужно подняться или спуститься по высоте дерева, а высота порядка Справочники:
Двоичная куча — Википедия
Priority queue — ВикипедияТиповые задачи под кучу
«всегда брать текущий минимальный элемент»
«сделать раз: взять лучшее, обновить, вернуть обратно»
«слить несколько отсортированных потоков»
«кратчайший путь во взвешенном графе»Минимальная куча в Python
Практическая деталь:
часто кладут пары (приоритет, вершина) или (стоимость, состояние)!Куча как дерево и как массив
Дейкстра: кратчайшие пути с неотрицательными весами
Формулировки, которые почти всегда ведут к Дейкстре:
«найдите кратчайший путь»
«минимальная стоимость добраться из A в B»
«граф взвешенный, веса неотрицательные»Ключевое ограничение:
веса рёбер должны быть неотрицательными, иначе стандартная Дейкстра может дать неверный ответИдея алгоритма
Мы поддерживаем текущие лучшие расстояния dist[v] и всегда выбираем вершину с минимальным dist среди ещё не обработанных. Это ровно то, что удобно делать кучей.
Что означает формула релаксации
Если есть ребро веса , то кандидат на улучшение:
Пояснение каждого элемента:
— уже найденная стоимость добраться до вершины
— стоимость перейти по ребру из в to
сумма — стоимость маршрута, который сначала идёт до , а потом делает один шаг в toЕсли new меньше текущего dist[to], мы улучшаем ответ.
Код Дейкстры (список смежности с весами)
Предполагаем, что g[v] — список пар (to, w).
Критически важная проверка if d != dist[v]:
в кучу могут попасть устаревшие записи
мы пропускаем их, чтобы не делать лишнюю работуСложность в типичной оценке:
из-за операций кучиСправочник:
Dijkstra Algorithm — CP-AlgorithmsТопологическая сортировка: порядок выполнения в DAG
Формулировки, которые почти всегда означают топологическую сортировку:
«есть зависимости между задачами»
«нужно упорядочить так, чтобы каждое ребро шло слева направо»
«найдите порядок курсов с пререквизитами»Топологическая сортировка существует только если ориентированный граф ацикличен (то есть это DAG).
Есть два классических подхода:
DFS с выходными временами
алгоритм Кана (через входные степени и очередь)На секции часто проще и надёжнее алгоритм Кана.
Алгоритм Кана (очередь + входные степени)
Идея:
посчитаем indeg[v] — сколько рёбер входит в вершину v
все вершины с indeg = 0 можно ставить первыми
удаляем их из графа (уменьшая indeg соседей), и процесс продолжаетсяКак понять, что есть цикл:
если мы не смогли извлечь все вершины (очередь опустела слишком рано), значит остались вершины с indeg > 0, то есть циклСправочник:
Topological Sorting — CP-AlgorithmsЧастые ошибки на секции
Путают представление графа: делают матрицу смежности при больших и ловят MLE.
Забывают visited или метку расстояния и получают бесконечные обходы по циклам.
В BFS неправильно инициализируют dist и теряют недостижимые вершины.
Запускают Дейкстру при отрицательных весах.
В Python используют рекурсивный DFS на длинной цепочке и получают ошибку глубины рекурсии.
В Дейкстре не отбрасывают устаревшие элементы из кучи и получают лишнюю работу.Мини-чеклист выбора техники
Граф невзвешенный, нужен кратчайший путь в рёбрах: BFS.
Граф, нужно просто обойти, найти компоненты, проверить достижимость: DFS/BFS.
Веса неотрицательные, нужен кратчайший путь по стоимости: Дейкстра + куча.
Есть зависимости, нужен порядок выполнения, и граф ориентированный: топологическая сортировка.
Структура ввода — дерево: обычно достаточно DFS/BFS, иногда с параметром parent.Эти темы логично продолжают предыдущие статьи курса: очередь и стек дают обходы, deque вы уже видели в окнах, а куча добавляет инструмент для задач, где нужно постоянно выбирать лучший следующий шаг. В сумме это покрывает значительную долю «графовой базы» алгоритмической секции.