Алгоритм Дейкстры в PascalABC.NET
В предыдущей статье о PriorityQueue мы познакомились с очередью с приоритетами. Одно из её классических применений — алгоритм Дейкстры, который находит кратчайшие пути от одной вершины графа до всех остальных.
Вес ребра может означать расстояние, время, стоимость проезда или задержку передачи данных — неотрицательную величину, которая складывается вдоль пути и которую требуется минимизировать. Поэтому алгоритм применяется в навигации, логистике, компьютерных сетях и игровых картах.
В этой статье используется компилятор PascalABC.NET для .NET 10: класс PriorityQueue доступен в современной библиотеке .NET, но отсутствует в .NET Framework 4.7.2.
Идея алгоритма
Пусть требуется найти кратчайшие расстояния от начальной вершины start. Для каждой вершины v будем хранить dist[v] — минимальное найденное к текущему моменту расстояние от start до v. Пока вершина не обработана, это расстояние ещё может улучшиться.
В начале известно только расстояние от стартовой вершины до самой себя:
На каждом шаге алгоритм выбирает ещё не обработанную вершину с минимальным известным расстоянием и рассматривает все выходящие из неё рёбра. Пусть из вершины u ведёт ребро веса w в вершину v. Если
то путь до v через u короче уже найденного. Обновим расстояние и добавим вершину в очередь с новым приоритетом:
if dist[u] + w < dist[v] then
begin
dist[v] := dist[u] + w;
q.Enqueue(v,dist[v])
end
Такое улучшение расстояния называется релаксацией ребра. Для быстрого выбора вершины с минимальным текущим расстоянием используется PriorityQueue: номер вершины помещается в очередь как элемент, а расстояние до неё — как приоритет.
Реализация
Рассмотрим неориентированный граф из шести вершин с номерами от 0 до 5. Программа запускает алгоритм Дейкстры из вершины 0 и вычисляет кратчайшее расстояние до каждой вершины графа. Сам маршрут она не восстанавливает — только его минимальную длину.
uses System.Collections.Generic;
begin
var n := 6;
var edges := [
(0,1,7), (0,2,9), (0,5,14),
(1,2,10), (1,3,15),
(2,3,12), (2,5,2),
(3,4,6), (4,5,9)
];
var graph := ArrGen(n,i -> new List<(integer,integer)>);
foreach var (u,v,w) in edges do
begin
graph[u].Add((v,w));
graph[v].Add((u,w))
end;
var start := 0;
var dist := ArrFill(n,integer.MaxValue);
var used := new boolean[n];
dist[start] := 0;
var q := new PriorityQueue<integer,integer>;
q.Enqueue(start,0);
while q.Count > 0 do
begin
var u := q.Dequeue;
if used[u] then
continue;
used[u] := True;
foreach var (v,w) in graph[u] do
if dist[u] + w < dist[v] then
begin
dist[v] := dist[u] + w;
q.Enqueue(v,dist[v])
end
end;
dist.Println
end.
Граф хранится в виде списков смежности: для каждой вершины хранится список пар «соседняя вершина, вес ребра». Выражение ArrGen создаёт отдельный список для каждой вершины.
Например, тройка (2,5,2) в массиве edges означает, что вершины 2 и 5 соединены ребром веса 2. Поскольку граф неориентированный, каждое ребро добавляется в списки обеих вершин. Для ориентированного графа следует оставить только graph[u].Add((v,w)) — добавление ребра в заданном направлении.
Что вычисляет программа
Результат работы программы:
0 7 9 21 20 11
Элементы массива соответствуют вершинам с номерами от 0 до 5:
| Вершина | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| Расстояние | 0 | 7 | 9 | 21 | 20 | 11 |
Например, кратчайшее расстояние от 0 до 1 равно , до 2 — , а до 5 — . Хотя существует непосредственное ребро веса , путь короче:
До вершины 4 кратчайшее расстояние равно : путь имеет длину
Таким образом, программа не просто находит ближайшую вершину и не ищет один заранее заданный маршрут. Она за один запуск вычисляет кратчайшие расстояния от выбранной стартовой вершины до всех остальных вершин графа.
Зачем нужна PriorityQueue
Без очереди с приоритетами на каждом шаге можно было бы перебирать все ещё не обработанные вершины и искать среди них вершину с минимальным dist. Такой выбор требует линейного просмотра массива. Для разреженных графов PriorityQueue позволяет организовать поиск эффективнее: Dequeue извлекает запись с наименьшим приоритетом, то есть с наименьшим расстоянием.
Есть важная особенность реализации: если для вершины сначала найден путь длины , а затем более короткий путь длины , вершина помещается в очередь ещё раз — уже с новым приоритетом. Старая запись остаётся в очереди.
Это происходит и в нашем примере: вершина 5 сначала добавляется с расстоянием , а после обработки вершины 2 — ещё раз с расстоянием . Запись с приоритетом будет извлечена раньше записи с приоритетом .
Поэтому одна вершина может оказаться в очереди несколько раз. Массив used позволяет обработать её только один раз, пропуская все последующие записи:
if used[u] then
continue;
Когда вершина впервые извлекается из очереди, её кратчайшее расстояние уже окончательно найдено. Почему? Выбрана необработанная вершина с минимальным известным расстоянием, а продолжение пути через другие необработанные вершины не может сделать его короче: все веса неотрицательны. После этого вершина отмечается как обработанная, а её рёбра просматриваются ровно один раз.
Ограничения и эффективность
Алгоритм Дейкстры работает только для графов с неотрицательными весами рёбер. Нулевые веса допустимы, отрицательные — нет. Это принципиальное условие корректности алгоритма.
В программе integer.MaxValue используется как условная бесконечность. Если вершина недостижима из start, её расстояние останется равным этому значению. В очередь попадают только вершины с найденным конечным расстоянием, поэтому условная бесконечность не участвует в сложении.
Для небольших весов в нашем примере типа integer достаточно. При больших весах или длинных путях сумма dist[u] + w может переполниться. В таких задачах нужно выбирать более широкий тип, например int64, и для расстояний, и для приоритетов очереди, а также проверять, что суммы помещаются в выбранный тип. Все реальные конечные расстояния должны быть меньше значения, выбранного в качестве бесконечности.
Обозначим число вершин через , а число рёбер — через . Для приведённой реализации с повторным добавлением вершин очередь может содержать записей. С учётом инициализации массивов оценка времени работы составляет
Для простого графа, без параллельных рёбер, её обычно записывают в привычной форме
Память для списков смежности, массивов и очереди составляет . Поэтому такая реализация особенно полезна для больших разреженных графов.
Если нужно освежить основные операции очереди, вернитесь к статье «PriorityQueue в PascalABC.NET: очередь с приоритетами в .NET 10».