Последняя проблема класса Algorithms 2 потребовала от нас написания алгоритма для вычисления кратчайшего пути между двумя узлами на графике и одним алгоритмом, который позволяет нам сделать это — Беллман-Форд .
Беллман-Форд вычисляет кратчайший путь из одного источника, что означает, что если у нас есть 5-вершинный граф, нам нужно будет запустить его 5 раз, чтобы найти кратчайший путь для каждой вершины, а затем найти кратчайшие пути из этих кратчайших путей.
Отличительной особенностью Bellman-Ford по сравнению с Djikstra является то, что он способен справляться с отрицательными весами.
Псевдокод алгоритма выглядит следующим образом:
- Пусть A = 2-D массив размером n * n, где n — количество вершин
- Инициализация A [0, s] = 0 ; А [0, v] = бесконечность для всех V ≠ ев , где s является узел мы находим кратчайший путь для
- для i = 1,2,3,… n-1
- для каждого v ∈ V
- A [i, v] = min {A [i-1, v],
min (w, v) ∈ E {A [k-1, w] + стоимость wv }
& nbsp} - где (w, v) — входящие ребра вершины v
- A [i, v] = min {A [i-1, v],
- для каждого v ∈ V
- проверить наличие отрицательных циклов, выполнив еще раз и проверив A [n] = A [n-1]
- Если они равны, верните A [n-1], в противном случае сообщите об отрицательном цикле.
Моя первая версия выглядела так:
import os
file = open(os.path.dirname(os.path.realpath(__file__)) + "/g_small.txt")
vertices, edges = map(lambda x: int(x), file.readline().replace("\n", "").split(" "))
adjacency_list = [[] for k in xrange(vertices)]
for line in file.readlines():
tail, head, weight = line.split(" ")
adjacency_list[int(head)-1].append({"from" : int(tail), "weight" : int(weight)})
s=0
cache = [[0 for k in xrange(vertices+1)] for j in xrange(vertices+1)]
cache[0][s] = 0
for v in range(0, vertices):
if v != s:
cache[0][v] = float("inf")
for i in range(1, vertices):
for v in range(0, vertices):
least_adjacent_cost = calculate_least_adjacent_cost(adjacency_list, i, v, cache[i-1])
cache[i][v] = min(cache[i-1][v], least_adjacent_cost)
# detecting negative cycles
for v in range(0, vertices):
least_adjacent_cost = calculate_least_adjacent_cost(adjacency_list, i, v, cache[vertices-1])
cache[vertices][v] = min(cache[vertices-1][v], least_adjacent_cost)
if(not cache[vertices] == cache[vertices-1]):
raise Exception("negative cycle detected")
shortest_path = min(cache[vertices-1])
print("Shortest Path: " + str(shortest_path))
где define_least_adjacent_cost определяется так:
def calculate_least_adjacent_cost(adjacency_list, i, v, cache):
adjacent_nodes = adjacency_list[v]
least_adjacent_cost = float("inf")
for node in adjacent_nodes:
adjacent_cost = cache[node["from"]-1] + node["weight"]
if adjacent_cost < least_adjacent_cost:
least_adjacent_cost = adjacent_cost
return least_adjacent_cost
Как вы можете видеть, мы используем список смежности в качестве представления графа, отображая каждый узел на соответствующие ребра. Затем мы инициализируем кэш согласно алгоритму, а затем имеем два вложенных цикла for, которые мы используем для определения кратчайшего пути от s до каждой вершины.
Функция calculate_least_adjacent_cost используется для определения того, какое из входящих ребер в вершине имеет наименьшую стоимость, с учетом предыдущих вычислений кратчайшего пути, которые мы сделали до исходных вершин этих входящих ребер.
Затем у нас есть дополнительный вызов в конце, чтобы проверить наличие отрицательных циклов. Если нет изменений в значениях, вычисленных по s для каждой вершины, мы знаем, что у нас нет отрицательных циклов, потому что в противном случае один из них вступил бы в силу и дал бы нам другой результат.
Этот алгоритм работает, но он неэффективен в использовании пространства — наш кэш имеет размер n * n, когда мы заботимся только о двух строках — предыдущей и текущей — поэтому мы можем просто использовать вместо этого список.
Если мы сделаем это, код теперь будет выглядеть так:
s=0
cache = [[] for j in xrange(vertices+1)]
cache[s] = 0
for v in range(0, vertices):
if v != s:
cache[v] = float("inf")
for i in range(1, vertices):
for v in range(0, vertices):
previous_cache = cache
least_adjacent_cost = calculate_least_adjacent_cost(adjacency_list, i, v, previous_cache)
cache[v] = min(previous_cache[v], least_adjacent_cost)
# detecting negative cycles
for v in range(0, vertices):
previous_cache = copy.deepcopy(cache)
least_adjacent_cost = calculate_least_adjacent_cost(adjacency_list, i, v, previous_cache)
cache[v] = min(previous_cache[v], least_adjacent_cost)
if(not cache == previous_cache):
raise Exception("negative cycle detected")
Делая это, мы теряем историю алгоритма во время выполнения, что означает, что в его текущем состоянии мы не сможем определить наш кратчайший путь, мы просто знаем его стоимость.
Для графа 1000, граничного графа 47978, требуется 27 секунд, чтобы пройти весь граф и обнаружить отрицательный цикл, если он есть.
Код для этого алгоритма на GitHub , как всегда.