Алгоритм Прима
Описание
Алгоритм Прима (Prim's algorithm) - это алгоритм построения минимального остовного дерева взвешенного связного неориентированного графа.
Минимальное остовное (покрывающее) дерево (МОД) в (неориентированном) связном взвешенном графе - это остовное дерево этого графа, имеющее минимальный возможный вес, где под весом дерева понимается сумма весов входящих в него ребер.

Еще один пример графа и его МОД. Каждое ребро помечено весом, который здесь примерно пропорционален длине ребра:

На этом рисунке видно, что в графе может быть более одного МОД.
Принцип работы
На вход алгоритма подается связный неориентированный граф. Для каждого ребра задается его стоимость.
Сначала берется произвольная вершина и находится ребро, инцидентное данной вершине и обладающее наименьшей стоимостью. Найденное ребро и соединяемые им две вершины образуют дерево. Затем, рассматриваются ребра графа, один конец которых - уже принадлежащая дереву вершина, а другой - нет; из этих ребер выбирается ребро наименьшей стоимости. Выбираемое на каждом шаге ребро присоединяется к дереву. Рост дерева происходит до тех пор, пока не будут исчерпаны все вершины исходного графа.
Результатом работы алгоритма является МОД.

Демонстрация работы алгоритма Прима на основе евклидова расстояния:

Псевдокод

Сложность
Асимптотика алгоритма зависит от способа хранения графа и способа хранения вершин, не входящих в дерево. Если приоритетная очередь Q
реализована как обычный массив d
, то Extract.Min(Q)
выполняется за O(n)
, а стоимость операции d|u| <- w(v, u)
составляет O(1)
. Если Q
представляет собой бинарную пирамиду, то стоимость Extract.Min(Q)
снижается до O(log n)
, а стоимость d|u| <- w(v, u)
возрастает до O(log n)
. При использовании фибоначчиевых пирамид операция Extract.Min(Q)
выполняется за O(log n)
, а d|u| <- w(v, u)
- за O(1)
.
Реализация
Для реализации алгоритма используется очередь с приоритетом:
// algorithms/graphs/prim.js
import Graph from '../../data-structures/graph/index'
import PriorityQueue from '../../data-structures/priority-queue'
// Функция принимает граф
export default function prim(graph) {
// При передаче направленного графа должно выбрасываться исключение
if (graph.isDirected) {
throw new Error('Алгоритм Прима работает только с ненаправленными графами')
}
// Инициализируем новый граф, который будет содержать
// минимальное остовное дерево (МОД) исходного графа
const minimumSpanningTree = new Graph()
// Эта очередь с приоритетом будет содержать все ребра,
// начинающиеся от посещенных узлов и ранжированные по весу -
// на каждом шаге мы всегда будем получать ребро с минимальным весом
const edgesQueue = new PriorityQueue()
// Набор посещенных узлов
const visited = {}
// Начальный узел для обхода графа
const startNode = graph.getAllNodes()[0]
// Добавляем начальный узел в набор посещенных узлов
visited[startNode.getKey()] = startNode
// Добавляем все ребра начального узла в очередь
startNode.getEdges().forEach((graphEdge) => {
edgesQueue.add(graphEdge, graphEdge.weight)
})
// Перебираем ребра, находящиеся в очереди
while (!edgesQueue.isEmpty()) {
// Извлекаем следующее ребро с минимальным весом
const currentMinEdge = edgesQueue.poll()
// Находим следующий непосещенный минимальный узел для обхода
let nextMinNode = null
if (!visited[currentMinEdge.from.getKey()]) {
nextMinNode = currentMinEdge.from
} else if (!visited[currentMinEdge.to.getKey()]) {
nextMinNode = currentMinEdge.to
}
// Пропускаем итерацию, если все узлы текущего ребра уже посещены
if (nextMinNode) {
// Добавляем текущее минимальное ребро в МОД
minimumSpanningTree.addEdge(currentMinEdge)
// Добавляем узел в посещенные
visited[nextMinNode.getKey()] = nextMinNode
// Добавляем ребра узла в очередь
nextMinNode.getEdges().forEach((graphEdge) => {
// Добавляем только те ребра, которые ведут к непосещенным узлам
if (
!visited[graphEdge.from.getKey()] ||
!visited[graphEdge.to.getKey()]
) {
edgesQueue.add(graphEdge, graphEdge.weight)
}
})
}
}
return minimumSpanningTree
}
Тесты
Запускаем тесты:
npm run test ./algorithms/graphs/__tests__/prim
