Редакция для Маршрут экспедиции
Submitting an official solution before solving the problem yourself is a bannable offence.
Автор:
1. Идея
Нужно найти путь минимальной суммарной длины в неориентированном взвешенном графе из лагеря s в лагерь t, а затем вывести не только длину, но и сам маршрут.
Так как все длины троп неотрицательны, для поиска кратчайших расстояний подходит алгоритм Дейкстры.
Чтобы потом восстановить маршрут, для каждой вершины будем хранить parent — из какой вершины мы пришли в неё по лучшему найденному пути. После завершения алгоритма можно пройти от t назад по массиву parent до s, а затем развернуть этот список.
2. Наблюдения
Граф неориентированный, значит каждую тропу
u v wнужно добавить в обе стороны:- из
uвv, - из
vвu.
- из
Вес ребра может быть равен
0, но это не мешает алгоритму Дейкстры: важно только, что отрицательных весов нет.Если
dist[t]так и осталось бесконечностью, то пути изsвtне существует, и нужно вывести-1.Если кратчайших путей несколько, можно вывести любой. Поэтому при улучшении расстояния достаточно просто запоминать текущего родителя.
Для больших ограничений
nдо100000иmдо300000нужен эффективный алгоритм. Полный перебор путей невозможен, а Дейкстра с очередью с приоритетом как раз подходит.
3. Алгоритм
- Считываем
n,m,s,t. - Строим список смежности
g, где для каждой вершины хранятся пары(to, w). - Создаём:
- массив
dist, гдеdist[v]— минимальное найденное расстояние отsдоv; - массив
parent, гдеparent[v]— предыдущая вершина на кратчайшем пути; - приоритетную очередь, в которой лежат пары
(расстояние, вершина).
- массив
- Инициализация:
dist[s] = 0,- кладём
(0, s)в очередь.
- Пока очередь не пуста:
- достаём вершину
vс минимальным текущим расстоянием; - если это расстояние не совпадает с
dist[v], значит запись устарела, её пропускаем; - перебираем все рёбра из
v; - если через
vможно улучшить расстояние доto, то:- обновляем
dist[to], - записываем
parent[to] = v, - добавляем новую пару в очередь.
- обновляем
- достаём вершину
- После завершения:
- если
dist[t]бесконечно, выводим-1; - иначе восстанавливаем путь:
- начинаем с
t, - переходим по
parentназад, - полученный список разворачиваем.
- начинаем с
- если
- Выводим:
- суммарную длину,
- количество лагерей в маршруте,
- сам маршрут.
4. Почему это работает
Алгоритм Дейкстры корректно работает в графах с неотрицательными весами.
Основная идея такая:
- в очереди всегда выбирается вершина с наименьшим текущим расстоянием;
- когда мы извлекаем вершину
vс расстояниемdist[v], это расстояние уже является кратчайшим; - после этого мы пытаемся улучшить расстояния до соседей через
v.
Почему можно восстанавливать путь через parent:
- когда мы улучшаем расстояние до вершины
toчерез вершинуv, мы запоминаемparent[to] = v; - значит, у каждой вершины хранится предыдущая вершина на лучшем найденном пути;
- для вершины
tпоследовательностьt -> parent[t] -> parent[parent[t]] -> ...приводит назад кs; - если развернуть эту последовательность, получится путь от
sдоt.
Почему путь действительно кратчайший:
parent[to]меняется только тогда, когда найден путь короче предыдущего;- в конце алгоритма
dist[t]— это длина кратчайшего пути доt; - цепочка
parentсоответствует именно тем переходам, которыми это расстояние было получено.
Почему проверка if cur_d != dist[v] важна:
- одна и та же вершина может несколько раз попадать в очередь;
- если позже нашёлся более короткий путь, старые записи становятся неактуальными;
- такие записи нужно игнорировать, чтобы не делать лишнюю работу.
5. Сложность
Пусть n — число лагерей, m — число троп.
- Построение графа:
O(m). - Алгоритм Дейкстры с приоритетной очередью:
O((n + m) log n). - Восстановление пути:
O(k), гдеk— число вершин в маршруте, иk <= n.
Итоговая сложность: O((n + m) log n).
Память:
- список смежности:
O(n + m), - массивы
distиparent:O(n), - очередь: до
O(m)в худшем случае.
Итоговая память: O(n + m).
6. Код на C++17
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
int main() {
int n, m, s, t;
cin >> n >> m >> s >> t;
vector<vector<pair<int, int>>> g(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
const long long INF = (long long)4e18;
vector<long long> dist(n + 1, INF);
vector<int> parent(n + 1, -1);
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int>>> pq;
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
long long cur_d = pq.top().first;
int v = pq.top().second;
pq.pop();
if (cur_d != dist[v]) {
continue;
}
for (auto edge : g[v]) {
int to = edge.first;
int w = edge.second;
if (dist[v] + w < dist[to]) {
dist[to] = dist[v] + w;
parent[to] = v;
pq.push({dist[to], to});
}
}
}
if (dist[t] == INF) {
cout << -1 << '\n';
return 0;
}
vector<int> path;
int cur = t;
while (cur != -1) {
path.push_back(cur);
cur = parent[cur];
}
reverse(path.begin(), path.end());
cout << dist[t] << '\n';
cout << path.size() << '\n';
for (int i = 0; i < (int)path.size(); i++) {
if (i > 0) {
cout << ' ';
}
cout << path[i];
}
cout << '\n';
return 0;
}
7. Код на Python 3
import heapq
n, m, s, t = map(int, input().split())
g = [[] for _ in range(n + 1)]
for _ in range(m):
u, v, w = map(int, input().split())
g[u].append((v, w))
g[v].append((u, w))
INF = 10**30
dist = [INF] * (n + 1)
parent = [-1] * (n + 1)
dist[s] = 0
pq = [(0, s)]
while pq:
cur_d, v = heapq.heappop(pq)
if cur_d != dist[v]:
continue
for to, w in g[v]:
nd = cur_d + w
if nd < dist[to]:
dist[to] = nd
parent[to] = v
heapq.heappush(pq, (nd, to))
if dist[t] == INF:
print(-1)
else:
path = []
cur = t
while cur != -1:
path.append(cur)
cur = parent[cur]
path.reverse()
print(dist[t])
print(len(path))
print(*path)
Комментарии