Статья Автор: Деникина Н.В., Деникин А.В.

Кратчайший путь в графе: Форд–Беллман

Алгоритм Форда–Беллмана


Идея: у каждой вершины есть рекорд

Заводим для каждой вершины рекорд — самую короткую известную на сейчас длину пути из единицы до неё. У самой единицы рекорд нулевой, у остальных — бесконечность.

Бесконечность

В питоне есть настоящая бесконечность: float('inf'). Она больше любого числа, а прибавление к ней ничего не меняет. Значит, если рекорд вершины i ещё бесконечность, то dist[i] + w тоже бесконечность и min ничего не испортит.

Бесконечность избавляет от проверки достижимости.

Одно ребро — одна попытка улучшить рекорд

Берём ребро из i в j весом w и рассуждаем: до i я дохожу за dist[i], значит до j могу дойти за dist[i] + w. Оставляем меньшее из двух — записанного и нового.

Сколько нужно проходов

Одного мало: строки лежат как попало, и ребро может встретиться раньше, чем станет известен его старт. Путь без повторов содержит не больше 999 рёбер, поэтому тысячи проходов хватает всегда.

Тысяча проходов по двумстам рёбрам — доли секунды.


Прогон руками по графу из условия

Рёбра берём в том порядке, в каком они лежат в файле.

реброрассуждениерекорды после шага
100 → 12рекорд сотни пока бесконечность, ничего не меняется1: 0
6 → 7рекорд шестёрки бесконечность, снова мимо1: 0
6 → 1мимо1: 0
1 → 70 + 5.5 меньше бесконечности — записываем1: 0, 7: 5.5
7 → 1005.5 + 2.0, записываем1: 0, 7: 5.5, 100: 7.5
4 → 100рекорд четвёрки бесконечность, мимобез изменений
1 → 1000 + 12.0 хуже рекорда 7.5, оставляем 7.5без изменений
1 → 40 + 2.5, записываем…, 4: 2.5

До вершины 12 рекорд не дошёл: когда смотрели ребро 100 → 12, у сотни ещё была бесконечность.

Что изменится на втором проходе?

Сработает только первое ребро: 100 → 12 даст 7.5 + 1.0 = 8.5. Остальные рекорды уже лучшие, и дальше ничего не меняется.

Итог: dist[100] = 7.5, ответ 7.

Тот же прогон по шагам

Шагай стрелками вперёд и назад или нажми «показать всё». Пунктирная стрелка — ребро, которое ничего не дало.


7.01.05.52.012.02.58.01.0671410012вершинарекорд1000000004—————2.52.52.56————————7——5.55.55.55.55.55.512——————8.58.5100———7.57.57.57.57.5Старт: знаем только, что до вершины 1 идти 0.Ребро 100 → 12: у вершины 100 рекорда нет, пропускаем.Ребро 1 → 7: 0 + 5.5, рекорда не было — записываем.Ребро 7 → 100: 5.5 + 2.0 = 7.5, записываем.Ребро 1 → 100: 12.0 хуже рекорда 7.5, не трогаем.Ребро 1 → 4: записываем 2.5. Первый проход закончен.Второй проход: теперь 100 → 12 срабатывает, 7.5 + 1.0 = 8.5.Третий проход ничего не изменил — стоп. Ответ: целая часть 7.5, то есть 7.

До вершины 6 рекорда не появится никогда: в неё не ведёт ни одна стрелка.


Сначала про бесконечность

Вся программа держится на том, как ведёт себя float('inf'). Проверим руками.



Результат работы программы
inf
True
7.5
inf
1000000000
999999996.0
inf

Бесконечность плюс вес остаётся бесконечностью — при любом знаке веса. Поэтому проверка «а добрались ли мы до начала ребра» не нужна вовсе.

Заглушка 10**9 ведёт себя иначе. С положительным весом она ещё держится, а на отрицательном ребре превращается в путь длиной 999999996 до вершины, куда дороги нет. В задаче про самый длинный путь она ломается сразу.


Алгоритм Форда-Беллмана. Реализация

Программа, которая считает кратчайший путь из вершины 1 в вершину 100. 

Описание некоторых строк
строка что делает
dist = [INF] * 1001 рекорды всех вершин, номер вершины — это индекс
dist[1] = 0 до стартовой вершины идти ноль
for step in range(1000) проходов с запасом: путь короче 1000 рёбер
for i, j, w in data берём рёбра по очереди, как они лежат в файле
dist[i] + w сколько выйдет, если дойти до i и шагнуть по ребру
min(dist[j], ...) оставляем лучшее из записанного и нового
int(dist[100] + 0.000001) целая часть с запасом на погрешность


Что печатает программа
30.799999999999997
30

Напечаталось 30.799999999999997, а не 30.8: из-за особенности хранения вещественных чисел в памяти компьютера.

Поэтому перед взятием целой части прибавляется крошечное число. Без него путь длиной ровно 31.0 мог бы накопиться как 30.999999999, и int() выдал бы 30 вместо 31.

Сначала печатай длину, потом целую часть.
 

Другой пример

Возьмём граф


Кратчайшее расстояние от 1 до 100 равно 31. Посмотрите ниже, что выдает код. 
 


Ловушки

  • Взяли 10**9 вместо float('inf'). На этой задаче сойдёт, но привычка опасная.
  • Сделали один проход вместо тысячи. Строки в файле не отсортированы.
  • Написали dist[j] = dist[i] + w без min. Запишется последнее ребро, а не лучшее.
  • Взяли int() без запаса и потеряли единицу на ровном месте.
  • Завели массив на 101 ячейку и получили выход за границы.

Проверь себя


Расставьте шаги программы в том порядке, в каком они выполняются.

Граф из условия: какими станут рекорды после второго прохода по списку рёбер?

Файл graph.txt: целая часть длины кратчайшего пути из вершины 1 в вершину 502.
Attached files
graph.txt
Печать