#!/usr/bin/env python
# coding: utf-8
# # Routing, Arbitrage
#
# Chapter 8 of [Real World Algorithms](https://mitpress.mit.edu/books/real-world-algorithms).
#
# ---
#
# > Panos Louridas
# > Athens University of Economics and Business
# ## The Bellman-Ford(-Moore) Algorithm
#
# The Bellman-Ford(-Moore) algorithm is an alternative to Dijkstra's algorithm for finding shortest paths in graphs.
#
# The basic idea is that we find the shortest paths from a start node to other nodes by using 1, 2, $n-1$ links, where $n$ is the number of nodes in the graph.
#
# So we start with shortest paths that have only one link, then with two links, and so on.
# We will use the following function to read weighted graphs from files containing one line per edge.
#
# This is familiar, it is the same that we used with Dijkstra's algorithm.
# In[1]:
def read_graph(filename, directed=False):
graph = {}
with open(filename) as input_file:
for line in input_file:
parts = line.split()
if len(parts) != 3:
continue # not a valid line, ignore
[n1, n2, w] = [ int (x) for x in parts ]
if n1 not in graph:
graph[n1] = []
if n2 not in graph:
graph[n2] = []
graph[n1].append((n2, w))
if not directed:
graph[n2].append((n1, w))
return graph
# As a first example we will use the traffic grid graph again.
#
#
# We go ahead and read it.
# In[2]:
import pprint
g = read_graph('traffic_grid_graph.txt')
pprint.pprint(g)
# We need a substitute for $\infty$ in the algorithm.
#
# We will use `sys.maxsize` as a substitute for $\infty$ .
#
# In Python there is no predefined maximum integer; `sys.maxsize` gives the maximum 32 bit or 64 bit integer, depending on the machine.
#
# If that is not enough, we could simply assign a larger value.
# In[3]:
import sys
MAX_INT = sys.maxsize
# The Bellman-Ford(-Moore) algorithm in Python is as follows; note that we just call it `bellman_ford(g, s)`.
# In[4]:
def bellman_ford(g, s):
nodes = g.keys()
num_nodes = len(nodes)
# Initialize array holding path lengths.
dist = [ MAX_INT ] * num_nodes
dist[s] = 0
# Initialize array holding node predecessors.
pred = [ -1 ] * num_nodes
# Try using paths of length up to num_nodes
for i in range(num_nodes - 1):
# Try all edges: get all nodes...
for u in nodes:
# ...then for each node get all edges.
for v, w in g[u]:
# If the path to node v is bigger than the path to
# node u and then the edge to v, update it.
# If the distance to u is MAX_INT, it means we
# have not reached it yet, so we should ignore it.
if dist[u] != MAX_INT and dist[v] > dist[u] + w:
dist[v] = dist[u] + w
pred[v] = u
return (pred, dist)
# We can apply it directly on our traffic grid graph:
# In[5]:
pred, dist = bellman_ford(g, 0)
print('pred', pred)
print('dist', dist)
# We can improve on our implementation by noting that at each iteration we do not need to check all edges.
#
# In particular, at each iteration of the algorithm, instead of checking all edges, we need to check only the edges of the nodes whose estimates we updated at the previous iteration.
#
# To do that, we will use a First-In First-Out (FIFO) queue, which we'll get from Python's [`deque`](https://docs.python.org/3/library/collections.html#collections.deque).
#
# Each time we update an edge we will add the target node in the queue.
#
# We will get the edges to check in each iteration by getting the edges of the first node in the queue each time.
#
# We will kick-off the process by adding the source node into the queue.
#
# We'll call the implementation `bellman_ford_qb(g, s)`, where `qb` stands for "queue-based".
#
# In[6]:
import collections
def bellman_ford_qb(g, s):
nodes = g.keys()
num_nodes = len(nodes)
# Initialize array holding path lengths.
dist = [ MAX_INT ] * num_nodes
dist[s] = 0
# Initialize array holding node predecessors.
pred = [ -1 ] * num_nodes
# Initialize queue.
q = collections.deque()
# We'll use a list to check whether something
# is already in the queue, so that we won't
# add it twice.
in_queue = [ False ] * num_nodes
# We start by putting the starting node in the queue.
in_queue[s] = True
q.append(s)
# While the queue is not empty:
while len(q) != 0:
u = q.popleft()
in_queue[u] = False
# For every edge of the current node, check
# and update if needed.
for (v, w) in g[u]:
if dist[u] != MAX_INT and dist[v] > dist[u] + w:
dist[v] = dist[u] + w
pred[v] = u
# Add the node of the updated path in the queue
# if necessary.
if in_queue[v] == False:
q.append(v)
in_queue[v] = True
return (pred, dist)
# Here are the results of the algorithm on the traffic grid example:
# In[7]:
pred, dist = bellman_ford_qb(g, 0)
print('pred', pred)
print('dist', dist)
# The Bellman-Ford(-Mooore) algorithm is generally slower than Dijkstra's, but it handle graphs with negative weights.
#
# Consider again the [negative_weights_graph.txt](negative_weights_graph.txt):
#
#
# We go ahead and read it:
# In[8]:
g = read_graph('negative_weights_graph.txt', directed=True)
pprint.pprint(g)
# Then if we try `bellman_ford(g, s)` on it, we'll see that we get the right results:
# In[9]:
pred, dist = bellman_ford(g, 0)
print('pred', pred)
print('dist', dist)
# The same with the queue-based version:
#
# In[10]:
pred, dist = bellman_ford_qb(g, 0)
print('pred', pred)
print('dist', dist)
# What about negative cycles?
#
# Consider the [negative_cycle_graph.txt](negative_cycle_graph.txt):
#
#
#
#
# We go ahead and read it:
# In[11]:
g = read_graph('negative_cycle_graph.txt', directed=True)
pprint.pprint(g)
# Then we try bellman_ford(g, s) on it, and see what we get:
# In[12]:
pred, dist = bellman_ford(g, 0)
print('pred', pred)
print('dist', dist)
# The algorithm terminates, but the results do not make much sense.
#
# For example, the distance to node 1 is -3, but in fact it is $-\infty$, as we can go round and round the negative cycle.
#
# The real problem is that we did not get any indication that the results are bogus because of negative cycles.
# To fix this problem, we will be adding to the queue a special, *sentinel* node.
#
# That is a fictitious node that does not exist in the graph.
#
# It will demarkate each set of neighbours that we add in the queue.
#
# Each time we meet it in the queue we will know that we have handled the complete set of neighbours of one node.
#
# This cannot happen more than $n$ times, where $n$ is the number of nodes; so if it does, we have reached a negative cycle.
# In[13]:
def bellman_ford_nc(g, s):
nodes = g.keys()
num_nodes = len(nodes)
# The sentinel is equal to the number of nodes,
# as the nodes start from 0.
sentinel = num_nodes
# Initialize array holding path lengths.
dist = [ MAX_INT ] * num_nodes
dist[s] = 0
# Initialize array holding node predecessors.
pred = [ -1 ] * num_nodes
q = collections.deque()
# We'll use a list to check whether something
# is already in the queue, so that we won't
# add it twice.
in_queue = [ False ] * num_nodes
in_queue[s] = True
# We start by putting the starting node and the
# sentinel in the queue.
q.append(s)
q.append(sentinel)
i = 1 # number of iterations
# Repeat as long as the queue contains more than one
# element (the sentinel) and we have not handled
# the neighbours of all nodes.
while len(q) != 1 and i < num_nodes:
u = q.popleft()
# If we have reached the sentinel, update the
# nodes count and put the sentinel back in the
# queue.
if u == sentinel:
i += 1
q.append(sentinel)
else:
in_queue[u] = False
# For every edge of the current node, check
# and update if needed.
for (v, w) in g[u]:
if dist[u] != MAX_INT and dist[v] > dist[u] + w:
dist[v] = dist[u] + w
pred[v] = u
# Add the node of the updated path in the queue
# if necessary.
if in_queue[v] == False:
q.append(v)
in_queue[v] = True
return (pred, dist, i < num_nodes)
# So now we are able to handle graphs with negative weights and detect cycles:
# In[14]:
pred, dist, no_negative_cycle = bellman_ford_nc(g, 0)
print('pred', pred)
print('dist', dist)
print('no_negative_cycle', no_negative_cycle)