Поиск кратчайших путей: Дейкстра и Беллман—Форд
Как эта тема связана с предыдущими
В статье про обходы мы увидели важный факт:
BFS на невзвешенном графе находит кратчайшие пути по числу рёбер. Но в реальных задачах рёбра часто имеют
стоимость:
расстояние (км)
время (мин)
цена
задержкаТогда «кратчайший» путь — это уже не «с наименьшим числом рёбер», а «с минимальной суммой весов». Для этого нужны алгоритмы для взвешенных графов.
В этой статье разберём два базовых алгоритма:
Дейкстра — быстрый и популярный, но требует неотрицательные веса
Беллман—Форд — медленнее, зато умеет работать с отрицательными весами и обнаруживает отрицательные циклыПостановка задачи: что такое кратчайший путь во взвешенном графе
Пусть есть путь из вершины
start в вершину
t:
start -> ... -> t
Если веса рёбер на этом пути равны , то стоимость пути (его «длина») равна:
Объяснение элементов формулы:
— суммарная стоимость пути
— знак суммы («сложить все»)
— номер ребра в пути
— количество рёбер в пути
— вес -го ребраЗадача кратчайшего пути: найти путь с минимальным .
Соглашение о представлении графа в Python
Как и в предыдущих статьях, будем использовать
список смежности, но для взвешенного графа:
Граф может быть ориентированным или неориентированным:
для ориентированного добавляем только u -> v
для неориентированного обычно добавляют оба направления: u -> v и v -> u с одним и тем же весом!Пример взвешенного графа и выделенный кратчайший путь
Алгоритм Дейкстры
Когда применять
Алгоритм Дейкстры применим, если:
все веса рёбер неотрицательные (то есть для каждого ребра)Он часто используется для:
навигации и логистики
кратчайших путей в сетях
задач «минимальной стоимости» без штрафов со знаком минусЕсли в графе есть отрицательное ребро, Дейкстра может выдать неверный ответ.
Интуиция
Дейкстра поддерживает текущие лучшие известные расстояния от
start до вершин. На каждом шаге он выбирает вершину с
минимальным текущим расстоянием среди ещё не обработанных и «фиксирует» это расстояние как окончательное.
Ключевой смысл неотрицательных весов:
если мы уже нашли самый дешёвый способ добраться до вершины v, то «дешевле позже» уже не получится, потому что любые дополнительные рёбра только увеличивают стоимость (или оставляют как есть при нулевом весе)Что такое релаксация
Для ребра
u -> v веса
w делаем проверку:
если dist[u] + w < dist[v], то мы нашли более короткий путь в v, и нужно обновить dist[v] и запомнить предка parent[v] = uЭто обновление обычно называют релаксацией.
Реализация на Python через heapq
Дейкстра требует структуру «достать вершину с минимальным расстоянием», поэтому удобно использовать
кучу (min-heap).
Документация: heapq — Heap queue algorithm.
Почему появляются «устаревшие» записи:
мы не умеем «уменьшать ключ» прямо внутри heapq
вместо этого мы добавляем новую пару (new_dist, вершина)
старая запись остаётся в куче, и при извлечении мы её игнорируемВосстановление пути
parent хранит предка на кратчайшем пути. Восстановление такое же по идее, как мы делали для BFS.
Сложность
При списке смежности и куче Дейкстра обычно работает быстро на больших разреженных графах. В учебной практике достаточно помнить:
он заметно быстрее Беллмана—Форда
но требует неотрицательных весовДополнительное чтение: Dijkstra's algorithm.
Алгоритм Беллмана—Форда
Когда применять
Беллман—Форд применяют, если:
в графе могут быть отрицательные веса
нужно обнаружить отрицательный цикл, который делает задачу «кратчайшего пути» некорректнойПример смысла отрицательного ребра:
«скидка», «кэшбэк», «прибыль» со знаком минус в модели стоимостиЧто такое отрицательный цикл и почему это проблема
Цикл — это маршрут, который возвращается в исходную вершину.
Если сумма весов по циклу отрицательна, то это отрицательный цикл. Тогда можно:
пройти цикл один раз и уменьшить стоимость пути
пройти цикл два раза и уменьшить ещё сильнее
и так бесконечноТо есть «самого короткого пути» не существует: стоимость может уходить к .
Интуиция
Беллман—Форд многократно делает релаксации всех рёбер.
Факт, на который он опирается:
если нет отрицательных циклов, то кратчайший путь до любой вершины содержит не более рёберОбъяснение элементов:
— множество вершин
— количество вершин
— «на одну меньше числа вершин»Почему так: если в пути было бы рёбер, то по принципу Дирихле какая-то вершина повторилась бы, значит есть цикл; цикл можно выбросить (если он не отрицательный), и путь не станет хуже.
Отсюда алгоритм:
повторить релаксацию всех рёбер раз
затем сделать ещё одну «контрольную» итерацию: если хоть что-то улучшается, значит есть отрицательный цикл, достижимый из startРеализация на Python
Удобно сначала получить список всех рёбер.
Что означает результат:
если третий результат True, то найден отрицательный цикл (и обычные кратчайшие пути не определены)
иначе dist и parent корректны, и путь можно восстанавливать функцией restore_pathДополнительное чтение: Bellman–Ford algorithm.
Сложность
Беллман—Форд обычно заметно медленнее Дейкстры, потому что многократно проходит по всем рёбрам. Его стоит выбирать, когда это действительно нужно из-за отрицательных весов или требования обнаружить отрицательный цикл.
Как выбрать алгоритм
| Ситуация | Что выбрать | Почему |
|---|---|---|
| Веса неотрицательные |
Дейкстра | быстрее и стандартен |
| Есть отрицательные веса |
Беллман—Форд | Дейкстра может ошибаться |
| Нужно обнаружить отрицательный цикл |
Беллман—Форд | имеет встроенную проверку |
| Невзвешенный граф (все веса = 1) |
BFS | проще и быстрее в этой частной задаче |
Мини-пример: сравнение на одном графе
Граф без отрицательных весов
Здесь кратчайший путь A -> C -> B -> D имеет стоимость .
Граф с отрицательным весом (без отрицательного цикла)
Если попытаться применить Дейкстру к такому графу, она не обязана работать правильно.
Частые ошибки
Запустили Дейкстру на графе с отрицательными весами: ответ может быть неверным.
Забыли про “устаревшие” элементы в куче в реализации Дейкстры на heapq: получите лишнюю работу или ошибки логики.
Неверно храните неориентированный граф: если ребро u—v, но вы добавили только u -> v, алгоритм будет считать граф ориентированным.
Интерпретируете “нет пути” как 0: обычно «нет пути» — это inf (в коде float("inf")).Что дальше
Теперь у вас есть полный базовый набор для путей:
BFS для невзвешенных графов
Дейкстра для взвешенных графов без отрицательных весов
Беллман—Форд для отрицательных весов и поиска отрицательных цикловСледующий естественный шаг после кратчайших путей — разбирать задачи, которые используют эти алгоритмы как подзадачу: маршрутизация, поиск компонент, проверка достижимости с ограничениями, а также более продвинутые алгоритмы для специальных классов графов.