3.2.1 — кратчайший путь от одной вершины до всех остальных3.2.1 — shortest path from one vertex to all others

Single-Source Shortest Path

BFS находит кратчайший путь по числу рёбер, но не работает, если у рёбер разный вес. Три алгоритма здесь решают именно взвешенную версию задачи, каждый — под свой набор допущений о графе: только неотрицательные веса (Дейкстра), любые веса (Беллман-Форд), и практическая оптимизация Беллмана-Форда, часто быстрая на реальных графах (SPFA).BFS finds the shortest path by edge count, but breaks down once edges carry different weights. The three algorithms here solve the weighted version of the problem, each under its own assumptions about the graph: non-negative weights only (Dijkstra), any weights (Bellman-Ford), and a practical optimization of Bellman-Ford that's often fast on real graphs (SPFA).

частый вопрос на собеседованииcommon interview question

Алгоритм ДейкстрыDijkstra's algorithm

Идея — жадная: поддерживать множество вершин с уже точно известным кратчайшим расстоянием от старта, и на каждом шаге добавлять в это множество вершину с минимальной оценкой расстояния среди ещё не добавленных (используя приоритетную очередь / min-heap). Корректность держится на том, что при неотрицательных весах расстояние до самой близкой ещё не обработанной вершины уже не может уменьшиться позже — если бы существовал более короткий путь к ней через ещё не обработанную вершину, эта вершина была бы ближе, что противоречит выбору минимума.The idea is greedy: maintain a set of vertices whose shortest distance from the source is already known for certain, and at each step add the vertex with the minimum distance estimate among those not yet added (using a priority queue / min-heap). Correctness rests on the fact that with non-negative weights, the distance to the closest not-yet-processed vertex can never decrease later — if a shorter path to it existed through another not-yet-processed vertex, that vertex would have to be closer, contradicting the choice of the minimum.

S:0 A:4 B:2 C:5 4 2 3 1 через B: 2+3=5, через A: 4+1=5via B: 2+3=5, via A: 4+1=5 обработанаsettled

порядок извлечения из очереди: S(0), B(2), A(4), C(5) — всегда минимум среди необработанныхextraction order from the queue: S(0), B(2), A(4), C(5) — always the minimum among unsettled vertices

std::vector<long long> dijkstra(int n, int src,
    const std::vector<std::vector<std::pair<int,int>>>& adj) { // (сосед, вес)// (neighbor, weight)
    std::vector<long long> dist(n, LLONG_MAX);
    std::priority_queue<std::pair<long long,int>,
        std::vector<std::pair<long long,int>>, std::greater<>> pq;
    dist[src] = 0; pq.push({0, src});
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue;              // устаревшая запись в куче// a stale heap entry
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    return dist;
}

Сложность с бинарной кучей — O((V+E) log V); с Fibonacci heap теоретически лучше — O(E + V log V), так как decrease-key там амортизированно O(1) вместо O(log V) — на практике константы Fibonacci heap делают её выигрышной только на очень больших разреженных графах, и большинство реальных реализаций (включая стандартные библиотеки) используют обычную бинарную кучу.Complexity with a binary heap is O((V+E) log V); with a Fibonacci heap it's theoretically better — O(E + V log V), since decrease-key there is amortized O(1) instead of O(log V) — in practice, a Fibonacci heap's constants only pay off on very large sparse graphs, and most real implementations (including standard libraries) use a plain binary heap.

Алгоритм Беллмана-ФордаBellman-Ford algorithm

Дейкстра ломается на отрицательных весах, потому что жадное предположение "обработанная вершина больше не изменится" перестаёт быть верным — более длинный по числу рёбер, но более "дешёвый" из-за отрицательного веса путь может появиться позже. Беллман-Форд решает это без жадности: V-1 раз проходит по всем рёбрам графа, пытаясь "расслабить" (relax) каждое — уменьшить dist[v], если dist[u] + вес(u,v) < dist[v]. V-1 итераций достаточно, потому что кратчайший путь в графе без отрицательных циклов не может содержать больше V-1 рёбер.Dijkstra breaks on negative weights because the greedy assumption "a settled vertex will never change" stops holding — a path with more edges but a cheaper total due to a negative weight can appear later. Bellman-Ford solves this without greediness: it passes over all of the graph's edges V-1 times, trying to "relax" each one — decreasing dist[v] if dist[u] + weight(u,v) < dist[v]. V-1 iterations suffice because a shortest path in a graph with no negative cycles can't contain more than V-1 edges.

Побочный эффект, который часто и есть настоящая причина использовать этот алгоритм: детекция отрицательных циклов. Если после V-1 итераций хотя бы одно ребро всё ещё можно расслабить, значит в графе есть цикл с отрицательной суммой весов, и понятие "кратчайший путь" для него не определено (можно бесконечно уменьшать стоимость, проходя цикл снова и снова).A side effect that's often the actual reason to use this algorithm: negative-cycle detection. If after V-1 iterations at least one edge can still be relaxed, the graph contains a cycle with a negative total weight, and "shortest path" is undefined for it (cost can be decreased indefinitely by looping through the cycle again and again).

bool bellmanFord(int n, int src, std::vector<std::array<int,3>>& edges,
    std::vector<long long>& dist) {  // edges: {u, v, weight}// edges: {u, v, weight}
    dist.assign(n, LLONG_MAX); dist[src] = 0;
    for (int i = 0; i < n - 1; ++i)
        for (auto& [u, v, w] : edges)
            if (dist[u] != LLONG_MAX && dist[u] + w < dist[v])
                dist[v] = dist[u] + w;
    for (auto& [u, v, w] : edges)                // V-й проход — детект цикла// V-th pass — cycle check
        if (dist[u] != LLONG_MAX && dist[u] + w < dist[v]) return false;
    return true;
}

SPFA (Shortest Path Faster Algorithm)SPFA (Shortest Path Faster Algorithm)

Наблюдение: Беллман-Форд честно проходит все E рёбер на каждой из V-1 итераций, даже когда абсолютное большинство рёбер давно не меняются. SPFA — оптимизация через очередь: вершина добавляется в очередь на обработку только если её dist только что уменьшился, и обрабатываются только вершины из очереди, а не все рёбра подряд. В среднем на практике это заметно быстрее O(VE), хотя в худшем случае (специально сконструированный граф) сложность остаётся O(VE) — по этой причине SPFA не имеет гарантий лучше Беллмана-Форда и на соревнованиях по алгоритмам считается риском, а не безопасной заменой.The observation: Bellman-Ford dutifully scans all E edges on each of its V-1 iterations, even when the vast majority of edges haven't changed in a while. SPFA optimizes via a queue: a vertex is enqueued for processing only when its dist has just decreased, and only queued vertices are processed, not every edge in sequence. On average, in practice, this is noticeably faster than O(VE), though in the worst case (a specially constructed graph) the complexity remains O(VE) — for this reason SPFA carries no better guarantee than Bellman-Ford, and in competitive programming it's treated as a risk, not a safe drop-in replacement.

5 примеров из реальной жизни5 real-world examples

01

Дейкстра — маршрутизация в GPS-навигаторах (базовый слой)Dijkstra — GPS navigation routing (the base layer)

Время или расстояние в пути всегда неотрицательны — идеальные условия для Дейкстры. Продакшен-навигаторы (Google Maps, OSRM) используют более сложные надстройки (A*, Contraction Hierarchies — см. 3.3.3), но все они — оптимизации именно этой базовой идеи, а не замена ей.Travel time or distance is always non-negative — ideal conditions for Dijkstra. Production navigators (Google Maps, OSRM) use more advanced layers on top (A*, Contraction Hierarchies — see 3.3.3), but all of them optimize this exact base idea rather than replace it.

02

Дейкстра — протокол маршрутизации OSPF в интернетеDijkstra — the OSPF internet routing protocol

Каждый роутер строит граф сети (соседние роутеры — вершины, качество канала — вес) и запускает Дейкстру локально, чтобы построить таблицу маршрутизации "кратчайший путь к каждой известной подсети". Это не учебный пример — это буквально то, как работает часть современного интернета.Every router builds a graph of the network (neighboring routers are vertices, link quality is the weight) and runs Dijkstra locally to build a routing table of "shortest path to every known subnet." This isn't a textbook example — it's literally how part of the modern internet works.

03

Беллман-Форд — арбитраж на валютном/крипто-рынкеBellman-Ford — arbitrage in currency/crypto markets

Курс обмена USD→EUR→JPY→USD, дающий в итоге больше денег, чем было — это отрицательный цикл в графе, где ребро — это -log(курс обмена) (логарифм превращает произведение курсов в сумму весов). Поиск такого цикла Беллманом-Фордом — стандартный алгоритм детекции арбитражных возможностей.An exchange chain USD→EUR→JPY→USD that ends with more money than it started with is a negative cycle in a graph where an edge is -log(exchange rate) (the logarithm turns a product of rates into a sum of weights). Finding such a cycle via Bellman-Ford is the standard algorithm for detecting arbitrage opportunities.

04

Беллман-Форд — протокол маршрутизации RIP (Routing Information Protocol)Bellman-Ford — the RIP (Routing Information Protocol) routing protocol

В отличие от OSPF (Дейкстра, полное знание топологии на каждом узле), RIP — distance-vector протокол, где каждый роутер знает только расстояния от соседей и итеративно их "расслабляет" — это распределённая версия ровно идеи Беллмана-Форда.Unlike OSPF (Dijkstra, full topology knowledge at every node), RIP is a distance-vector protocol, where each router only knows distances from its neighbors and iteratively "relaxes" them — a distributed version of exactly Bellman-Ford's idea.

05

SPFA/Беллман-Форд — планировщик задач с отрицательными ограничениями (разностные ограничения)SPFA/Bellman-Ford — a task scheduler with negative constraints (difference constraints)

Задачи вида "событие B должно случиться не раньше A" (t_B ≥ t_A + d) моделируются как система разностных ограничений, которая сводится к графу кратчайших путей — некоторые ограничения дают отрицательные веса рёбер, что делает Дейкстру неприменимой и требует именно Беллмана-Форда.Constraints like "event B must happen no earlier than A" (t_B ≥ t_A + d) are modeled as a system of difference constraints, which reduces to a shortest-path graph — some constraints produce negative edge weights, ruling out Dijkstra and requiring Bellman-Ford specifically.

ГраблиGotchas

Дейкстра на графе с отрицательным весом даёт неверный ответ без ошибкиDijkstra on a graph with a negative weight gives a wrong answer with no error

Дейкстра не падает и не бросает исключение на отрицательном весе — она просто может "зафиксировать" вершину как обработанную до того, как найдётся более короткий путь через отрицательное ребро, и никогда не пересмотрит это решение. Это тихая логическая ошибка, а не runtime-сбой, что делает её особенно опасной в проде.Dijkstra doesn't crash or throw on a negative weight — it can simply "lock in" a vertex as settled before a shorter path through a negative edge is found, and it never revisits that decision. This is a silent logic error, not a runtime failure, which makes it especially dangerous in production.

Забыть проверку "устаревшей записи" в куче ДейкстрыForgetting the "stale entry" check in Dijkstra's heap

Стандартная реализация без decrease-key (как в примере выше) допускает несколько записей одной вершины в куче с разными dist — без проверки `if (d > dist[u]) continue;` алгоритм пересчитает соседей уже обработанной вершины повторно, что не ломает корректность, но превращает O((V+E) log V) в существенно менее предсказуемую по факту производительность.A standard implementation without decrease-key (as in the example above) allows multiple heap entries for the same vertex with different dist values — without the `if (d > dist[u]) continue;` check, the algorithm reprocesses an already-settled vertex's neighbors again, which doesn't break correctness but turns O((V+E) log V) into meaningfully less predictable actual performance.

Не проверить наличие отрицательного цикла перед тем, как доверять результату Беллмана-ФордаNot checking for a negative cycle before trusting Bellman-Ford's result

Код, который запускает V-1 итераций и сразу использует dist[], без финального V-го прохода на детект цикла, молча выдаёт бессмысленное число для вершин, достижимых из отрицательного цикла — "кратчайший путь" для них математически не существует, и его нужно явно трактовать как ошибку, а не как результат.Code that runs V-1 iterations and immediately uses dist[], skipping the final V-th cycle-detection pass, silently returns a meaningless number for vertices reachable from a negative cycle — "shortest path" mathematically doesn't exist for them, and this must be explicitly treated as an error, not a result.