0Pricing
Coding Interview Prep · Урок

Алгоритм Дейкстры с очередью с приоритетами

Реализуйте алгоритм Дейкстры с помощью heapq, проследите шаги релаксации на взвешенном графе и решите задачу о самых дешёвых перелётах не более чем с k пересадками

«Алгоритм Дейкстры с очередью с приоритетами» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.

Кратчайший путь во взвешенных графах

Алгоритм Дейкстры находит кратчайшие пути от одной исходной вершины до всех остальных вершин во взвешенном графе с неотрицательными весами рёбер. Он жадно обрабатывает вершины в порядке их текущего наилучшего известного расстояния, каждый раз расширяя ближайшую ещё не посещённую вершину. Ключевая структура данных — это минимальная куча (очередь с приоритетом), которая эффективно извлекает вершину с наименьшим расстоянием.

Обзор шагов алгоритма

Алгоритм Дейкстры: (1) инициализируйте dist[source] = 0, а для всех остальных вершин установите dist[all others] = inf. (2) Поместите (0, source) в минимальную кучу. (3) Извлеките вершину u с наименьшим расстоянием. Если она уже была посещена с меньшим расстоянием, пропустите её. (4) Для каждого соседа v вершины u: если dist[u] + weight(u,v) < dist[v], обновите dist[v] и поместите (dist[v], v) в кучу. (5) Повторяйте, пока куча не опустеет.

Реализация на Python с помощью модуля двоичной кучи

Модуль Python heapq реализует минимальную кучу. Представим граф в виде списка смежности: graph[u] = [(v, weight), ...]. В куче хранятся кортежи (distance, node). Мы используем множество посещённых вершин visited, чтобы пропускать устаревшие записи в куче — записи, добавленные до того, как был найден более короткий путь.

import heapq

def dijkstra(graph, source):
    n = len(graph)
    dist = [float('inf')] * n
    dist[source] = 0
    heap = [(0, source)]  # (distance, node)
    visited = set()
    
    while heap:
        d, u = heapq.heappop(heap)
        if u in visited:
            continue
        visited.add(u)
        
        for v, weight in graph[u]:
            if dist[u] + weight < dist[v]:
                dist[v] = dist[u] + weight
                heapq.heappush(heap, (dist[v], v))
    
    return dist

Разбор примера

Рассмотрим граф с 5 вершинами и рёбрами: 0→1 (4), 0→2 (1), 2→1 (2), 1→3 (1), 2→3 (5), 3→4 (3). Кратчайшие пути из вершины 0: до 1 через 0→2→1 со стоимостью 3, до 2 со стоимостью 1, до 3 через 0→2→1→3 со стоимостью 4, до 4 через 0→2→1→3→4 со стоимостью 7. Алгоритм Дейкстры находит все эти пути за один проход, а не только путь к одной целевой вершине.

import heapq

def dijkstra(graph, source):
    dist = [float('inf')] * len(graph)
    dist[source] = 0
    heap = [(0, source)]
    visited = set()
    while heap:
        d, u = heapq.heappop(heap)
        if u in visited:
            continue
        visited.add(u)
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist

graph = [
    [(1,4),(2,1)],  # 0
    [(3,1)],         # 1
    [(1,2),(3,5)],   # 2
    [(4,3)],         # 3
    []               # 4
]
print(dijkstra(graph, 0))  # [0, 3, 1, 4, 7]

Почему алгоритм Дейкстры не работает с отрицательными весами

Корректность алгоритма Дейкстры зависит от того, что после извлечения вершины из минимальной кучи её расстояние становится окончательным. Это верно только в том случае, если веса рёбер неотрицательны. При наличии отрицательного ребра u→v с весом -5 после посещения v мы можем обнаружить более короткий путь через u, но v уже помечена как посещённая. Одного отрицательного ребра достаточно, чтобы сделать все последующие вычисления расстояний неверными.

Самые дешёвые перелёты с максимумом K пересадок (LeetCode 787)

В этой задаче появляется ограничение: не более k пересадок. Стандартный алгоритм Дейкстры не учитывает количество переходов напрямую. Решение: расширить состояние до (cost, node, stops_remaining). Используйте алгоритм Дейкстры с этим кортежем или алгоритм Беллмана—Форда с k+1 проходами релаксации. Модифицированный алгоритм Дейкстры останавливается, когда stops_remaining достигает 0, предотвращая дальнейшие переходы.

import heapq
from collections import defaultdict

def findCheapestPrice(n, flights, src, dst, k):
    graph = defaultdict(list)
    for u, v, w in flights:
        graph[u].append((v, w))
    
    heap = [(0, src, k + 1)]  # (cost, node, hops_left)
    visited = {}  # node -> min hops_left seen at this cost level
    
    while heap:
        cost, node, hops = heapq.heappop(heap)
        if node == dst:
            return cost
        if hops == 0:
            continue
        if visited.get(node, 0) >= hops:
            continue
        visited[node] = hops
        for nxt, w in graph[node]:
            heapq.heappush(heap, (cost + w, nxt, hops - 1))
    return -1

print(findCheapestPrice(4,[[0,1,100],[1,2,100],[0,2,500]],0,2,1))  # 200

Анализ временной сложности

С двоичной кучей алгоритм Дейкстры работает за O((V + E) log V): каждая вершина извлекается один раз (V извлечений), каждое ребро может привести к добавлению элемента (E добавлений), а каждая операция с кучей стоит O(log V). С кучей Фибоначчи оценка улучшается до O(E + V log V), но heapq в Python представляет собой двоичную кучу. Для разреженных графов (E ≈ V) вариант с двоичной кучей работает за O(V log V); для плотных графов (E ≈ V²) — за O(V² log V).

Восстановление кратчайшего пути

Чтобы восстановить сам путь, а не только расстояния, поддерживайте массив prev: при обновлении dist[v] устанавливайте prev[v] = u. После завершения алгоритма восстановите путь от источника до назначения, двигаясь назад: начните с dst, следуйте по указателям prev до source, а затем разверните полученный результат.

import heapq

def dijkstra_path(graph, source, target):
    n = len(graph)
    dist = [float('inf')] * n
    prev = [-1] * n
    dist[source] = 0
    heap = [(0, source)]
    visited = set()
    while heap:
        d, u = heapq.heappop(heap)
        if u in visited: continue
        visited.add(u)
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                prev[v] = u
                heapq.heappush(heap, (dist[v], v))
    # Reconstruct
    path, node = [], target
    while node != -1:
        path.append(node)
        node = prev[node]
    return dist[target], path[::-1]

Использование словаря для разреженных графов

Если вершины представлены строками или непоследовательными целыми числами, используйте defaultdict(list) для списка смежности и обычный dict для расстояний. Это часто встречается в задачах LeetCode, например «Задержка в сети», где вершины пронумерованы от 1 до n. Не забудьте использовать dist = {node: inf for node in all_nodes} и проверить наличие недостижимых вершин после завершения алгоритма.

import heapq
from collections import defaultdict

def networkDelayTime(times, n, k):
    graph = defaultdict(list)
    for u, v, w in times:
        graph[u].append((v, w))
    
    dist = {i: float('inf') for i in range(1, n+1)}
    dist[k] = 0
    heap = [(0, k)]
    
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]: continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    
    ans = max(dist.values())
    return ans if ans < float('inf') else -1

print(networkDelayTime([[2,1,1],[2,3,1],[3,4,1]], 4, 2))  # 2

Сравнение с BFS для невзвешенных графов

Для невзвешенных графов BFS находит кратчайшие пути за O(V + E), то есть быстрее, чем алгоритм Дейкстры за O((V+E) log V). Алгоритм Дейкстры обобщает BFS для взвешенных графов, используя очередь с приоритетом вместо обычной очереди FIFO. Если веса всех рёбер одинаковы, алгоритм Дейкстры сводится к BFS. Выбирайте BFS для невзвешенных графов, алгоритм Дейкстры — для неотрицательных весов, а Беллмана—Форда — для отрицательных весов.

Алгоритм Дейкстры с оптимизацией уменьшения ключа

В учебном варианте алгоритма Дейкстры используется очередь с приоритетом и операцией уменьшения ключа: когда расстояние до вершины улучшается, её приоритет обновляется на месте. Для этого нужна куча Фибоначчи, обеспечивающая сложность O(E + V log V), но реализовать её трудно. При используемом на собеседованиях подходе с ленивым удалением вместо этого добавляется новая запись, а устаревшие извлечённые элементы пропускаются — это проще и требует лишь постоянного множителя дополнительных затрат. В Python ленивое удаление с помощью heapq является стандартной реализацией алгоритма для собеседований.

Быстрая проверка

Проверьте своё понимание концепций структур данных и алгоритмов — подготовки к собеседованию по программированию, рассмотренных в этом уроке.

Итоги урока

В этом уроке вы узнали: алгоритм Дейкстры использует минимальную кучу для жадной обработки вершин в порядке возрастания текущего наилучшего расстояния, он работает за O((V+E) log V) и не справляется с рёбрами отрицательного веса, а устаревшие записи в куче обрабатываются проверкой множества посещённых вершин при извлечении. Далее мы рассмотрим алгоритм Беллмана—Форда, который обрабатывает отрицательные веса с помощью n-1 проходов релаксации.

Часто задаваемые вопросы

Урок «Алгоритм Дейкстры с очередью с приоритетами» бесплатный?

Да — полный текст урока «Алгоритм Дейкстры с очередью с приоритетами» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Алгоритм Дейкстры с очередью с приоритетами»?

Реализуйте алгоритм Дейкстры с помощью heapq, проследите шаги релаксации на взвешенном графе и решите задачу о самых дешёвых перелётах не более чем с k пересадками Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Алгоритм Дейкстры с очередью с приоритетами»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Алгоритм Дейкстры с очередью с приоритетами
  2. Беллман—Форд и отрицательные циклы
  3. Флойд—Уоршелл: кратчайшие пути между всеми парами
  4. Задержка в сети и восстановление пути
← Назад к Coding Interview Prep