# теория · шаг 2 из 7
Отрицательные циклы: когда у задачи нет ответа
Коротко
Если в графе есть цикл отрицательного веса и он достижим из старта, кратчайшего пути ПРОСТО НЕ
СУЩЕСТВУЕТ: накручивая цикл, путь можно удешевлять бесконечно. Это не сбой алгоритма, а свойство
задачи, и правильное поведение программы - не выдать число, а сообщить «ответа нет». Обнаружить
цикл Беллман-Форд умеет бесплатно: после V − 1 прохода все честные расстояния уже посчитаны,
поэтому если V-й проход всё ещё что-то улучшает, значит отрицательный цикл есть. Мало того,
его можно и предъявить: вершину, улучшенную на последнем проходе, прогоняют V шагов назад по
prev (чтобы гарантированно попасть внутрь цикла) и идут дальше до повтора.
Если ты знаешь критерий «V-й проход улучшает ⇒ отрицательный цикл», умеешь восстановить сам цикл и понимаешь, почему при нём задача теряет смысл, листай до «Теперь сам».
Как думаешь, что должна вернуть программа
Граф: s→a = 1, a→b = −2, b→a = −1. Сумма весов цикла a→b→a равна −3. Прежде чем читать
дальше, посчитай: чему равно кратчайшее расстояние от s до b? Пройди цикл один раз, потом два,
потом десять - и посмотри, к чему всё идёт.
На что это похоже
Обменник, где цепочка обменов возвращает тебе больше денег, чем было. Вопрос «какая самая дешёвая последовательность обменов» теряет смысл: крутись по кругу - и богатей. Правильный ответ такой системе не «вот минимум», а «здесь бесконечная прибыль, вот она». Кстати, это не выдумка: поиск такого цикла в графе курсов валют и есть классическая задача про арбитраж.
Механизм: V-й проход как детектор
def bellman_ford_with_check(start):
dist = [INF] * V; dist[start] = 0
prev = [-1] * V
for _ in range(V - 1):
for (u, v, w) in edges:
if dist[u] < INF and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
prev[v] = u
# проверочный проход: всё честное уже посчитано
for (u, v, w) in edges:
if dist[u] < INF and dist[u] + w < dist[v]:
return None, v, prev # улучшение есть → отрицательный цикл, v внутри «зоны» цикла
return dist, None, prev
Восстановление самого цикла - это продолжение той же функции (или отдельная, которой передали v и
prev; массивы V, edges, prev те же):
x = v # вершина, улучшенная на проверочном проходе
for _ in range(V):
x = prev[x] # V шагов назад гарантированно вводят внутрь цикла
cycle = [x]
y = prev[x]
while y != x:
cycle.append(y)
y = prev[y]
cycle.reverse() # вершины цикла по порядку
Почему V шагов: вершина v необязательно лежит на самом цикле - она может быть «ниже по
течению», просто её оценка подешевела из-за него. Но каждый шаг по prev уводит на одно ребро
назад по пути, а путь, улучшаемый бесконечно, обязан упираться в цикл; V шагов заведомо больше,
чем длина «хвоста», поэтому после них мы точно внутри цикла.
Правило
Отрицательный цикл, достижимый из старта, означает, что кратчайших путей НЕ СУЩЕСТВУЕТ: длину
можно уменьшать бесконечно. Критерий: после V − 1 прохода Беллмана-Форда сделать ещё один,
ПРОВЕРОЧНЫЙ, - если он улучшает хоть одну оценку, отрицательный цикл есть. Сам цикл восстанавливают,
пройдя V шагов назад по prev от улучшенной вершины (это гарантированно вводит внутрь цикла) и
затем двигаясь до повтора. Цикл, НЕдостижимый из старта, на ответ не влияет - чтобы найти любой,
запускают алгоритм из фиктивной вершины, соединённой рёбрами нулевого веса со всеми.
Разбор: почему ответа нет
Вернёмся к вопросу из начала: s→a = 1, a→b = −2, b→a = −1.
Путь s→a→b стоит 1 + (−2) = −1. Но можно крутануть цикл: s→a→b→a→b стоит
1 − 2 − 1 − 2 = −4. Ещё круг: −7. Каждый оборот отнимает 3. Значит для любого числа M
найдётся путь дешевле −M, и минимума не существует - множество длин не ограничено снизу.
Поэтому вопрос «чему равно dist[b]» некорректен, и алгоритм, который вернёт какое-нибудь число,
соврёт. Правильный ответ программы - признак «есть отрицательный цикл» (и, если полезно, сам цикл).
Заметь важную оговорку: значение имеет ДОСТИЖИМОСТЬ. Если отрицательный цикл висит в стороне и из
старта в него не попасть, расстояния от старта прекрасно определены, и Беллман-Форд посчитает их
верно; проверочный проход при этом ничего не улучшит, потому что оценки вершин цикла так и
остались бесконечностями. А если надо найти отрицательный цикл ГДЕ УГОДНО в графе, добавляют
фиктивную вершину с рёбрами нулевого веса во все остальные и запускают алгоритм из неё - тогда
достижимо всё. Расстояния она не портит: рёбра ведут только ИЗ неё, поэтому ни один путь между
старыми вершинами через неё не проходит, а новых циклов она не создаёт по той же причине - в неё
не входит ни одно ребро. Единственное, что меняется: dist считается теперь не от s, а от
фиктивного старта, и как расстояния эти числа читать уже нельзя - они нужны только затем, чтобы
проверочный проход увидел цикл.
Разбор: ловим цикл проверочным проходом
Граф: s→a = 1, a→b = 2, b→c = −1, c→a = −3 (цикл a→b→c→a весит 2 − 1 − 3 = −2),
V = 4, значит три обычных прохода и один проверочный. Рёбра в порядке записи.
проход 1: dist[a]=1, dist[b]=3, dist[c]=2, затем c→a: 2−3 = −1 < 1 → dist[a] = −1
проход 2: a→b: −1+2 = 1 < 3 → dist[b] = 1; b→c: 0 < 2 → dist[c] = 0; c→a: −3 < −1 → dist[a] = −3
проход 3: то же самое ещё раз - каждый проход отнимает вес цикла, то есть 2:
dist[b] = −1, dist[c] = −2, dist[a] = −5
проверочный проход: a→b даёт −3 < −1 → УЛУЧШЕНИЕ ЕСТЬ → отрицательный цикл
Видно, как оценки уезжают вниз без остановки. Сдвиг одинаков у всех трёх вершин цикла и равен
ровно весу цикла (−2): за проход оценка каждой вершины успевает пройти по кругу один раз, поэтому
dist[b] падает с 1 до −1, dist[c] с 0 до −2, dist[a] с −3 до −5. Теперь
восстановим сам цикл: улучшенная вершина - b; делаем V = 4 шага назад по prev
(b → a → c → b → a) и оказываемся внутри цикла; дальше идём по prev до повтора и получаем
a → c → b → a, то есть цикл a → b → c → a (в обратную сторону, останется развернуть).
Что ломается без этого
Первое: сделать V проходов вместо V − 1 и не различать последний - тогда цикл не будет
обнаружен, а оценки окажутся «какими-то», и ошибка станет молчащей. Второе: проверять цикл
условием «оценка стала отрицательной» - неверно, отрицательные оценки бывают и без всяких циклов.
Третье: объявлять цикл по любому улучшению в середине работы - улучшения идут постоянно, критерий
только про ПРОВЕРОЧНЫЙ проход после V − 1 штатных. Четвёртое: искать отрицательный цикл в графе
целиком, запуская алгоритм из одной вершины: цикл в другой компоненте останется незамеченным -
нужна фиктивная вершина. Пятое: восстанавливать цикл, начиная прямо с улучшенной вершины, без V
шагов назад: она может лежать вне цикла, и ты выпишешь «хвост» вместо цикла.
Зачем это в работе
Самое известное применение - валютный арбитраж. Берут граф валют, вес ребра - минус логарифм
курса; тогда произведение курсов превращается в сумму весов, а цепочка обменов с прибылью - ровно в
отрицательный цикл. Поиск такого цикла Беллманом-Фордом и есть детектор арбитража. Второе
применение - системы разностных ограничений (x_j − x_i <= c): они сводятся к кратчайшим путям, и
отрицательный цикл означает, что система НЕСОВМЕСТНА, то есть ограничения противоречат друг другу.
Так проверяют расписания и планировщики: «задача B должна начаться не позже, чем через 3 часа после
A» - набор таких условий либо выполним, либо содержит замкнутое противоречие, и оно предъявляется
как цикл. Третье - поиск ошибок в моделях: отрицательный цикл в графе стоимости часто означает не
выгодную возможность, а ошибку в данных (перепутанный знак, дважды учтённая скидка).
Теперь сам
Граф: s→a = 2, a→b = 1, b→a = −4, b→t = 3. Есть ли отрицательный цикл, достижим ли он, и
что должна вернуть программа? Что изменится, если вес b→a сделать равным −1?
Ответ. Цикл здесь один: a → b → a с весом 1 + (−4) = −3 - отрицательный. Достижим ли
он из старта? Да: s→a ведёт прямо в него. Значит кратчайших расстояний до a, b и t не
существует - каждый оборот цикла отнимает 3, и путь можно удешевлять бесконечно. Программа обязана
вернуть не числа, а признак «отрицательный цикл» (и, если нужно, сам цикл a → b → a).
Проверочный проход это и обнаружит: после трёх штатных проходов (V = 4) четвёртый всё ещё будет
улучшать оценки.
Если b→a сделать равным −1, вес цикла станет 1 + (−1) = 0 - цикл перестанет быть
отрицательным. Кратчайшие пути появятся: dist[s] = 0, dist[a] = 2, dist[b] = 3,
dist[t] = 6; накручивать нулевой цикл бессмысленно - длина от этого не уменьшается. Проверочный
проход не найдёт ни одного улучшения. Вот и граница: дело не в наличии отрицательных РЁБЕР (они
есть в обоих случаях), а в знаке суммы по циклу.
Главное
Отрицательный цикл, достижимый из старта, уничтожает саму задачу: крутясь по нему, путь удешевляют
бесконечно, и минимума не существует. Правильное поведение программы - сообщить об этом, а не
выдать число. Беллман-Форд обнаруживает цикл бесплатно: после V − 1 штатного прохода делают ещё
один, ПРОВЕРОЧНЫЙ, и любое улучшение на нём означает отрицательный цикл (важно: критерий именно про
этот проход, а не про «оценка стала отрицательной» - отрицательные оценки бывают и без циклов).
Сам цикл восстанавливают, пройдя V шагов назад по prev от улучшенной вершины - она может лежать
вне цикла, а V шагов гарантированно вводят внутрь, - и дальше двигаясь до повтора. Цикл,
недостижимый из старта, на ответ не влияет; чтобы найти отрицательный цикл где угодно в графе,
добавляют фиктивную вершину с рёбрами нулевого веса во все остальные. Отсюда же и практическая
ценность: арбитраж валют (минус логарифм курса), проверка совместности систем разностных
ограничений, поиск противоречий в расписаниях.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий