ДИСКРЕТНАЯ МАТЕМАТИКА

Кратчайшие пути между всеми парами вершин
(Алгоритм Флойда — Уоршелла)

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

ilspo@edu:~/dm/floyd-warshall
$ cat README.md # Floyd–Warshall algorithm Алгоритм работает за Θ(n³) времени и использует Θ(n²) памяти. Разработан в 1962 году. $ → изучайте материал ниже
~/theory

Теория и методология алгоритма Флойда — Уоршелла

Постановка задачи

Дан взвешенный ориентированный граф G(V, E), вершины которого пронумерованы от 1 до n. Вес ребра u→v равен ω(u,v), если ребро существует, и +∞ в противном случае. Требуется найти матрицу кратчайших расстояний d, где d[i][j] — длина кратчайшего пути из i в j, либо +∞, если j недостижима из i.

Идея алгоритма

Обозначим через d(i)uv длину кратчайшего пути между u и v, использующего в качестве промежуточных только вершины из множества {1..i}. Тогда справедливо рекуррентное соотношение:

d(i)uv = min( d(i−1)uv , d(i−1)ui + d(i−1)iv )

Если кратчайший путь из u в v, проходящий только через вершины {1..i}, использует вершину i, то он состоит из кратчайшего пути u→i и кратчайшего пути i→v. Иначе он совпадает с кратчайшим путём, использующим лишь вершины {1..i−1}. Перебирая вершины i = 1..n по очереди и пересчитывая все пары (u, v), после n шагов получаем итоговую матрицу кратчайших расстояний.

Псевдокод (в первом приближении)

Прямая реализация хранит трёхмерный массив d(i)[u][v] и работает за Θ(n³) времени, используя при этом Θ(n³) памяти:

d[0][u][v] = w[u][v]
for i in V:
    for u in V:
        for v in V:
            d[i][u][v] = min(d[i-1][u][v], d[i-1][u][i] + d[i-1][i][v])

Оптимизация памяти: окончательная версия

Можно избавиться от индекса i и использовать один двумерный массив d[u][v], обновляя его «на месте». Корректность обеспечивается инвариантом ρ(u,v) ≤ d[u][v] ≤ d(i)uv: значения в массиве никогда не занижают истинное расстояние и монотонно к нему сходятся. Такая реализация всё ещё работает за Θ(n³), но требует уже только Θ(n²) памяти.

Реализация на Python

INF = float('inf')

def floyd_warshall(graph):
    n = len(graph)
    d = [row[:] for row in graph]   # копия матрицы весов
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if d[i][k] + d[k][j] < d[i][j]:
                    d[i][j] = d[i][k] + d[k][j]
    return d

graph = [
    [0,   1,   6, INF],
    [INF, 0,   4,   1],
    [INF, INF, 0, INF],
    [INF, INF, 1,   0]
]

for row in floyd_warshall(graph):
    print(row)

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

Чтобы получить не только длину, но и сам путь, заводят дополнительный массив next, хранящий следующую вершину на кратчайшем пути из u в v:

def floyd_warshall_with_path(graph, nxt):
    n = len(graph)
    d = [row[:] for row in graph]
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if d[i][k] + d[k][j] < d[i][j]:
                    d[i][j] = d[i][k] + d[k][j]
                    nxt[i][j] = nxt[i][k]
    return d, nxt

def get_path(u, v, d, nxt):
    if d[u][v] == INF:
        return None                  # пути между u и v нет
    path = [u]
    while u != v:
        u = nxt[u][v]
        path.append(u)
    return path

Поиск отрицательного цикла

Если в графе есть цикл отрицательного веса, после завершения алгоритма на главной диагонали матрицы d появится хотя бы одно отрицательное число: d[i][i] < 0 означает, что через вершину i проходит цикл суммарно отрицательного веса. Чтобы не допустить переполнения при экспоненциальном уменьшении расстояний, значения дополнительно ограничивают снизу величиной −∞.

Построение транзитивного замыкания

Та же схема применима для отношений: если W[i][j] = 1 означает наличие ребра, то после выполнения W[i][j] = W[i][j] or (W[i][k] and W[k][j]) для всех троек i, j, k матрица W превращается в транзитивное замыкание отношения — W[i][j] = 1 тогда и только тогда, когда из i в j существует путь. Строки W можно упаковать в битовые маски, ускоряя внутренний цикл в 32 или 64 раза.

Сложность и ограничения

  • Время работы: Θ(n³) независимо от числа рёбер — алгоритм невыгоден для разреженных графов, где n одиночных запусков Дейкстры могут оказаться быстрее.
  • Память: Θ(n²) в оптимизированной версии.
  • Корректно работает с отрицательными весами рёбер, но не с отрицательными циклами — при их наличии он позволяет их обнаружить, а не корректно посчитать расстояния.
  • Константа в Θ(n³) очень мала — используются только сложение и сравнение, поэтому на практике алгоритм проще и часто быстрее асимптотически более сложных методов при небольших n.
~/diagrams

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

Таблица 1. Исходная матрица весов графа из примера на Python (4 вершины: 1, 2, 3, 4; «×» — главная диагональ, «∞» — ребро отсутствует)

  1 2 3 4
1 × 1 6
2 × 4 1
3 ×
4 1 ×

Таблица 2. Изменение матрицы d по шагам (k — номер очередной промежуточной вершины); полужирным выделены значения, изменившиеся на этом шаге

Шаг Матрица d (строки 1–4, столбцы 1–4)
k = 0
(нач.)
[×,1,6,∞] · [∞,×,4,1] · [∞,∞,×,∞] · [∞,∞,1,×]
k = 1 без изменений — из вершины 1 нет входящих рёбер
k = 2 d[1][3] = min(6, 1+4) = 5,   d[1][4] = min(∞, 1+1) = 2
k = 3 без изменений — из вершины 3 нет исходящих рёбер, кроме диагонали
k = 4 d[1][3] = min(5, 2+1) = 3,   d[2][3] = min(4, 1+1) = 2

Таблица 3. Итоговая матрица кратчайших расстояний (результат floyd_warshall(graph))

  1 2 3 4
1 0 1 3 2
2 0 2 1
3 0
4 1 0

[0, 1, 3, 2] · [inf, 0, 2, 1] · [inf, inf, 0, inf] · [inf, inf, 1, 0]

Исполняющий код Python алгоритм Флойда — Уоршелла

Пример реализации на python

← Алгоритм Дейкстры Назад в раздел "Образование"