Как найти самый дешёвый путь, если переходы между вершинами имеют разную стоимость? Научимся выбирать вершину, улучшать расстояния и записывать алгоритм на C++ и Python.
Неотрицательные веса
Очередь с приоритетом
C++17 и Python 3
Практика CSES и Codeforces
1. Кратчайший — значит с минимальной суммой весов
Представь карту дорог. Вершины — города, рёбра — дороги, а вес ребра — время или цена поездки. Стоимость маршрута — сумма весов всех его рёбер.
Прямой путь 1 → 2
8
Одно ребро, стоимость 8.
Путь 1 → 3 → 2
2 + 1 = 3
Два ребра, но стоимость меньше.
BFS ищет путь с наименьшим числом рёбер. Когда все веса одинаковы, этого достаточно. При разных неотрицательных весах используем алгоритм Дейкстры: он находит минимальные стоимости путей от одного старта до всех вершин.
Условие применения: каждый вес должен быть ≥ 0. Вес 0 разрешён. При отрицательных весах Дейкстра может ошибиться — для таких графов нужны другие алгоритмы.
2. Три вещи, которые храним
Название
Что означает
dist[v]
Лучшая известная стоимость пути от старта до v. Сначала ∞, а для старта — 0.
parent[v]
Из какой вершины пришли по лучшему найденному пути. Нужен для восстановления маршрута; сначала −1.
q
Очередь с приоритетом. Хранит пары (расстояние, вершина) и первой выдаёт пару с минимальным расстоянием.
∞ — условное обозначение «путь ещё не найден». В C++ используем большое число, в Python — float("inf"). Нулевой элемент массивов не используем: номера вершин начинаются с 1.
Почему подойдёт не обычная очередь?
Обычная очередь извлекает записи в порядке добавления. Дейкстре нужна запись с минимальной стоимостью, даже если она добавлена позже. Эту операцию быстро выполняет двоичная куча — основа очереди с приоритетом.
3. Как выполняется алгоритм
Положи dist[s] = 0 и добавь в очередь пару (0, s).
Извлеки минимальную пару (du, v). Здесь du — расстояние, записанное в этой паре.
Если du != dist[v], запись устарела. Пропусти её и извлеки следующую.
Для каждого ребра v → u веса w вычисли nd = du + w. Если nd < dist[u], обнови dist[u], запиши parent[u] = v и добавь новую пару (nd, u).
Повторяй, пока очередь не опустеет.
Попытка улучшить расстояние через ребро называется релаксацией. Используем строгое сравнение <: равный по стоимости путь не требует новой записи.
Почему можно доверять извлечённому актуальному минимуму?
Любой ещё не проверенный путь должен пройти через вершину, до которой добираться не дешевле текущего минимума. Дальнейшие рёбра не уменьшают стоимость, ведь их веса неотрицательны. Поэтому после проверки актуальности расстояние до выбранной вершины окончательное.
4. Один пример — весь ход решения
Начнём в вершине 1. Рёбра на рисунке можно проходить в обе стороны. Число рядом с ребром — его вес.
Не путай вес ребра с расстоянием от старта.
dist после проверки всех соседей выбранной вершины
Вершина
1
2
3
4
5
6
Старт
0
∞
∞
∞
∞
∞
1
0
8
2
∞
∞
∞
3
0
3
2
9
8
∞
2
0
3
2
5
8
∞
4
0
3
2
5
6
11
5
0
3
2
5
6
8
6
0
3
2
5
6
8
До 2 сначала нашли путь стоимости 8. Через 3 получили 2 + 1 = 3. Новая пара (3, 2) попадёт в очередь, а старая (8, 2) останется там и позже будет пропущена.
Кратчайший путь до 6: 1 → 3 → 2 → 4 → 5 → 6. Его стоимость: 2 + 1 + 2 + 1 + 2 = 8.
В списке adj[v] храним пары «сосед, вес». Для направленного ребра добавляем только a → b. Для ненаправленного — и a → b, и b → a.
C++17
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <utility>
#include <algorithm>
using namespace std;
// n — число вершин, s — старт (номера от 1 до n).
// adj[v] содержит пары {сосед, вес}.
const long long INF = 1LL << 62;
vector<long long> dist(n + 1, INF);
vector<int> parent(n + 1, -1);
using State = pair<long long, int>;
priority_queue<State, vector<State>, greater<State>> q;
Python 3
from heapq import heappush, heappop
# n — число вершин, s — старт (номера от 1 до n).
# adj[v] содержит пары (сосед, вес).
INF = float("inf")
dist = [INF] * (n + 1)
parent = [-1] * (n + 1)
q = []
C++: обычная priority_queue выдаёт максимум. Параметр greater<State> превращает её в очередь минимума. long long нужен для больших сумм весов. Python:heapq уже работает как очередь минимума; целые числа не ограничены 32 битами.
Основная часть алгоритма
C++17
dist[s] = 0;
q.push({0, s});
while (!q.empty()) {
auto [du, v] = q.top();
q.pop();
if (du != dist[v]) {
continue;
}
for (auto [u, w] : adj[v]) {
long long nd = du + w;
if (nd < dist[u]) {
dist[u] = nd;
parent[u] = v;
q.push({nd, u});
}
}
}
Python 3
dist[s] = 0
heappush(q, (0, s))
while q:
du, v = heappop(q)
if du != dist[v]:
continue
for u, w in adj[v]:
nd = du + w
if nd < dist[u]:
dist[u] = nd
parent[u] = v
heappush(q, (nd, u))
Действие
C++
Python
Добавить пару
q.push({d, v})
heappush(q, (d, v))
Извлечь минимум
q.top(), затем q.pop()
heappop(q)
Очередь не пуста
!q.empty()
bool(q), в цикле — while q
В обоих языках пары сравниваются сначала по расстоянию, затем по номеру вершины. При равных расстояниях порядок обработки вершин не меняет итоговую стоимость путей.
6. Проверь себя
Улучшится ли расстояние?
До v уже известно расстояние 4. Ребро v → u имеет вес 3. Сейчас dist[u] = 10. Какое новое значение запишем в dist[u]?
Разобрать ответ
4 + 3 = 7. Так как 7 < 10, запишем 7 и добавим пару (7, u).
Какую запись извлечём?
В очереди пары (8, 2), (2, 3), (5, 4). Выбери следующую.
Разобрать ответ
Пара (2, 3): выбираем минимальное расстояние 2, а не минимальный номер вершины.
Старая запись
Извлекли (8, 2), но dist[2] = 3. Что делать?
Разобрать ответ
8 ≠ 3: запись устарела. Пропускаем её, сохраняя уже найденное расстояние 3.
Где применим Дейкстру?
Один граф содержит веса 0, 2, 5; второй — веса −2, 3, 7. Выбери правильное утверждение.
Разобрать ответ
Подходит первый граф: нулевые веса разрешены, отрицательные — нет.
7. Готовые программы: прочитай граф и найди расстояния
Формат: n вершин, m направленных рёбер, затем m строк a b w. Старт — вершина 1. Выводим расстояния до всех вершин, для недостижимых — −1. В задаче CSES Shortest Routes I все вершины достижимы, поэтому −1 не появится.
Ввод
3 4
1 2 6
1 3 2
3 2 3
1 3 4
Вывод
0 5 2
Открыть полные программы C++17 и Python 3
C++17
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <utility>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<pair<int, long long>>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int a, b;
long long w;
cin >> a >> b >> w;
adj[a].push_back({b, w}); // направленное ребро
}
const long long INF = 1LL << 62;
vector<long long> dist(n + 1, INF);
vector<int> parent(n + 1, -1);
using State = pair<long long, int>;
priority_queue<State, vector<State>, greater<State>> q;
int s = 1;
dist[s] = 0;
q.push({0, s});
while (!q.empty()) {
auto [du, v] = q.top();
q.pop();
if (du != dist[v]) {
continue;
}
for (auto [u, w] : adj[v]) {
long long nd = du + w;
if (nd < dist[u]) {
dist[u] = nd;
parent[u] = v;
q.push({nd, u});
}
}
}
for (int v = 1; v <= n; ++v) {
cout << (dist[v] == INF ? -1 : dist[v]);
cout << (v == n ? '\n' : ' ');
}
}
Python 3
import sys
from heapq import heappush, heappop
input = sys.stdin.buffer.readline
n, m = map(int, input().split())
adj = [[] for _ in range(n + 1)]
for _ in range(m):
a, b, w = map(int, input().split())
adj[a].append((b, w)) # направленное ребро
INF = float("inf")
dist = [INF] * (n + 1)
parent = [-1] * (n + 1)
q = []
s = 1
dist[s] = 0
heappush(q, (0, s))
while q:
du, v = heappop(q)
if du != dist[v]:
continue
for u, w in adj[v]:
nd = du + w
if nd < dist[u]:
dist[u] = nd
parent[u] = v
heappush(q, (nd, u))
print(*(d if d != INF else -1 for d in dist[1:]))
В C++ выбранное INF больше возможных сумм в приведённых задачах. Для других ограничений отдельно проверь, что INF больше любого ответа и сложение не переполняет long long.
8. Как получить сам маршрут
При улучшении расстояния записываем parent[u] = v. После завершения алгоритма начинаем с цели t, переходим по parent до старта и переворачиваем полученную последовательность.
В примере: 6 ← 5 ← 4 ← 2 ← 3 ← 1. После разворота получим маршрут 1 → 3 → 2 → 4 → 5 → 6. Если старт совпадает с целью, маршрут состоит из одной вершины и стоит 0.
Код восстановления пути к вершине t
Вставь после алгоритма, когда t уже задана. Для C++ нужен заголовок <algorithm>.
C++17
if (dist[t] == INF) {
cout << -1; // пути нет
} else {
vector<int> path;
for (int v = t; v != -1; v = parent[v])
path.push_back(v);
reverse(path.begin(), path.end());
for (int v : path) cout << v << ' ';
}
Python 3
if dist[t] == INF:
print(-1) # пути нет
else:
path = []
v = t
while v != -1:
path.append(v)
v = parent[v]
path.reverse()
print(*path)
9. Что чаще всего мешает решению
Зафиксировать вершину при первом обнаружении. Первый найденный путь может оказаться длиннее следующего. Окончательное расстояние получаем при извлечении актуального минимума.
Забыть старые записи. Проверка du != dist[v] пропускает записи, для которых расстояние уже улучшилось.
Сравнивать только вес ребра. Нужна вся стоимость: du + w.
Перепутать направление. Авиарейс a → b не означает, что существует рейс b → a.
Хранить большие расстояния в int. В C++ используй long long для весов и сумм.
Насколько быстрый этот вариант?
Для n вершин и m рёбер реализация с повторными записями работает за O(n + m·log(m+1)) и использует O(n + m) памяти. Каждое успешное улучшение добавляет запись; очередь обрабатывает их с помощью кучи. Для обычного простого графа оценку часто записывают как O((n+m)·log n).
10. Задачи для практики
Начни с Shortest Routes I. Затем попробуй Dijkstra? с восстановлением пути. Остальные задачи — продолжение: они добавляют новые состояния или дополнительную информацию о маршрутах.
Ниже — краткие условия на русском, английские названия и ссылки на оригинал для отправки решения. Точные примеры и формат проверки доступны на странице задачи.
Первый запуск
CSES 1671 · Кратчайшие маршруты I (Shortest Routes I)
Есть n городов и m направленных авиарейсов. Рейс a → b имеет длину c. Найди минимальные длины маршрутов из города 1 во все города. До каждого города можно добраться.
Ввод, вывод и ограничения
Ввод: Сначала n и m, затем m строк a b c.
Вывод: n чисел: расстояния до городов 1, 2, …, n.
1 ≤ n ≤ 10⁵; 1 ≤ m ≤ 2·10⁵; 1 ≤ c ≤ 10⁹.
Подсказка после самостоятельной попытки
Обычный Дейкстра от вершины 1. Программы выше подходят для этой задачи.
Найди минимальную стоимость перелёта из города 1 в город n. Рейсы направленные. Есть купон: на одном рейсе с ценой c можно заплатить ⌊c/2⌋. Использовать купон можно не более одного раза. Маршрут существует.
Ввод, вывод и ограничения
Ввод: n и m, затем m строк a b c.
Вывод: Одно число — минимальная стоимость с учётом купона.
2 ≤ n ≤ 10⁵; 1 ≤ m ≤ 2·10⁵; 1 ≤ c ≤ 10⁹.
Подсказка после самостоятельной попытки
Для каждого города храни два расстояния: купон ещё не использован и уже использован. Это Дейкстра по состояниям.
Codeforces 449B · Джжху и города (Jzzhu and Cities)
Есть n городов, столица — город 1. Их соединяют m двусторонних дорог. Ещё k двусторонних железнодорожных маршрутов ведут из столицы в указанные города. Закрой как можно больше железнодорожных маршрутов, сохранив кратчайшие расстояния от столицы до всех городов.
Ввод, вывод и ограничения
Ввод: n, m, k; затем m строк u v x с дорогами; затем k строк s y с железнодорожными маршрутами из 1 в s.
Вывод: Максимальное число железнодорожных маршрутов, которые можно закрыть.
2 ≤ n ≤ 10⁵; 1 ≤ m ≤ 3·10⁵; 1 ≤ k ≤ 10⁵; веса 1…10⁹. Все города достижимы; повторяющиеся дороги и маршруты допустимы.
Подсказка после самостоятельной попытки
Дейкстра найдёт расстояния. Дополнительно нужно учесть одинаково короткие пути по дорогам и повторяющиеся железнодорожные маршруты.
В направленной сети авиарейсов найди самый дешёвый перелёт из города 1 в город n. Дополнительно выясни число самых дешёвых маршрутов, а также минимум и максимум числа рейсов среди них. Все цены положительные; маршрут существует.
Ввод, вывод и ограничения
Ввод: n и m, затем m строк a b c.
Вывод: Четыре числа: минимальная цена; число самых дешёвых маршрутов по модулю 10⁹+7; минимальное и максимальное число рейсов в них.
1 ≤ n ≤ 10⁵; 1 ≤ m ≤ 2·10⁵; 1 ≤ c ≤ 10⁹.
Подсказка после самостоятельной попытки
Расширь Дейкстру: вместе с расстоянием храни количество путей и две оценки числа рейсов. Отдельно обработай равную стоимость.
Города соединены двусторонними железнодорожными участками. Билет на концерт в городе j стоит aⱼ. Для каждого города i найди минимальную сумму: добраться до выбранного концерта, купить билет и вернуться в i. Можно остаться на концерт в своём городе.
Ввод, вывод и ограничения
Ввод: n и m, затем m строк u v w с участками, затем n цен a₁…aₙ.
Вывод: n чисел — минимальные затраты для каждого исходного города.
2 ≤ n ≤ 2·10⁵; 1 ≤ m ≤ 2·10⁵; веса и цены 1…10¹². Петель и повторяющихся участков нет.
Подсказка после самостоятельной попытки
Ответ для i равен minⱼ(aⱼ + 2·d(i,j)). Запусти Дейкстру сразу из всех городов: начальное dist[j] = aⱼ, а вес каждого ребра удвой.