.. bellman-ford-algorithm Bellman-Ford ============ The Bellman-Ford algorithm is one of the fundamental algorithms in graph theory for finding the shortest path from a source vertex to all other vertices in a weighted graph. Unlike Dijkstra's algorithm, this method can handle graphs with **negative edge weights**, provided there are no negative cycles reachable from the source vertex. Mathematical Background ----------------------- Let :math:`G = (V, E)` be a directed graph with edge weights :math:`w(e)`. For each vertex :math:`v \in V`, the algorithm maintains a distance estimate :math:`d[v]`, initialized to :math:`\infty` except for the source vertex :math:`s` where :math:`d[s] = 0`. The algorithm performs relaxation on all edges :math:`|V| - 1` times. For each edge :math:`(u, v) \in E`, the relaxation step is: .. math:: d[v] = \min(d[v], d[u] + w(u, v)) If after :math:`|V| - 1` iterations, a shorter path is still found, the graph contains a negative cycle. Algorithm Steps -------------- 1. Initialize distances from the source to all vertices as infinity, except the source itself (distance 0). 2. Repeat |V| - 1 times: a. For each edge (u, v) in the graph: i. If d[v] > d[u] + weight(u, v), update d[v]. 3. Check for negative cycles by iterating through all edges again. If any distance can be updated, a negative cycle exists. Implementation -------------- Below is a Python implementation of the Bellman-Ford algorithm: .. code-block:: python :linenos: def bellman_ford(graph, source): # Initialize distance from source node distance = {node: float('infinity') for node in graph} distance[source] = 0 # Relax all edges |V| - 1 times for _ in range(len(graph) - 1): for u in graph: for v, w in graph[u].items(): # Update distance if shorter path found if distance[u] + w < distance[v]: distance[v] = distance[u] + w # Check for negative cycles for u in graph: for v, w in graph[u].items(): if distance[u] + w < distance[v]: raise ValueError("Graph contains a negative cycle") return distance .. image:: images/bellman-ford-example.png :alt: Bellman-Ford execution example Key Properties -------------- - **Time Complexity**: O(|V| * |E|) - **Space Complexity**: O(|V|) - **Correctness**: Guaranteed to find shortest paths if no negative cycles exist. - **Use Cases**: Network routing, currency arbitrage detection, and systems with negative weights. Label Correcting Approach ------------------------ Bellman-Ford is considered a "label correcting" algorithm, as it iteratively updates distance labels until no further improvements are possible. This contrasts with "label setting" algorithms like Dijkstra's, which permanently fix distances once they are determined. Problem Statement ----------- We have a directed weighted graph :math:`G`. The weights of the edges in :math:`G` can also be negative. We know that graph :math:`G` contains no cycles with negative total weight. Now, for each vertex, we want to find the length of the shortest path from source vertex :math:`sc` to all other vertices, where the length of a path is equal to the sum of its edge weights. .. code-block:: python # This is a code example, comments remain in Finglish def bellman_ford(graph, start): distance = {node: float('infinity') for node in graph} distance[start] = 0 Bellman-Ford Algorithm ---------------------- To solve this problem, we first define a :math:`dp` table with dimensions :math:`|V(G)| \times |V(G)|` where :math:`dp_{i,j}` represents the length of the shortest walk from vertex :math:`sc` to vertex :math:`j` with at most :math:`i` edges. We know that the length of the shortest walk between two vertices in graph :math:`G` is equal to the length of the shortest path. This is because if the shortest walk contains a repeated vertex, it implies the existence of a cycle with a positive length (according to the problem's assumption, cycles cannot have negative lengths). This cycle can be removed to obtain a shorter walk, which is a contradiction. For the base cases of this DP table, we have :math:`dp_{i, sc} = 0` and :math:`dp_{0, u \neq sc} = \infty`. To compute :math:`dp_{i, j}`, we consider all incoming edges to :math:`j`. For each edge :math:`e` from :math:`u_e` to :math:`j` with weight :math:`w_e`, we calculate the value :math:`dp_{i-1, u_e} + w_e`. The minimum of these values gives the solution for :math:`dp_{i, j}`. Thus: .. math:: dp_{i, j} = \displaystyle{\min_{\forall \, e \: \in \: N_{j}^{-}(j)}} dp_{i-1, u_e} + w_e To find the length of the shortest path from vertex :math:`sc` to vertex :math:`u`, the value :math:`dp_{n-1, u}` suffices. This is because a path from :math:`sc` to any other vertex can have at most :math:`n` vertices and :math:`n-1` edges. Based on the DP definition and the fact that the shortest walk between two vertices in :math:`G` is necessarily a path, this value is exactly the required one. .. image:: /images/bellman-ford.png :align: center Order Analysis -------------- To update all cells of :math:`dp_i` for each vertex, we perform operations proportional to its in-degree. We know that the sum of in-degrees of all vertices equals :math:`|E(G)|` (edge count). Therefore in total, we've performed :math:`\mathcal{O}\left(|V(G)|.|E(G)|\right)` operations. The memory order used is also :math:`\mathcal{O}\left(|V(G)|^2\right)` . Memory Order Optimization ------------------------- To optimize the amount of memory used, the first dimension can be removed. As a result, the memory complexity becomes :math:`\mathcal{O}\left(|V(G)|\right)`. Then, for each edge, :math:`|V(E)| - 1` steps are performed to update the **dp** value of the destination vertex of the edge. By "updating", we mean that if edge :math:`e` goes from vertex :math:`u_e` to :math:`v_e` with weight :math:`w_e`, we set: :math:`dp_{v_e} = \min(dp_{v_e}, dp_{u_e} + w_e)`. Now, a question arises: Does performing this operation keep the **dp** entries at their desired values? The answer is yes. If we consider the **dp** from the previous section as :math:`dp^{\prime}`, after the :math:`i`-th update step of **dp**, we know that :math:`dp_u` equals one of :math:`dp_{i, u}^{\prime}, dp_{i+1, u}^{\prime}, \dots, dp_{n-1, u}^{\prime}` (the proof of this lemma is left to the reader). Now, if we check after the :math:`n-1`-th step, we find that :math:`dp_u = dp_{n-1, u}^{\prime}`. Therefore, after the :math:`n-1`-th step, the **dp** entries have the desired values. Finding the Shortest Path ------------------------- After all this discussion, a question arises: if we want the optimal path itself from :math:`sc` to another vertex like :math:`des`, what should we do? To solve this question, we need to slightly modify the previous algorithm. We consider an auxiliary array called :math:`par` with :math:`|V(G)|` elements. Initially, we set all elements of :math:`par` to -1. Now, when we consider an edge :math:`e` and :math:`dp_{v_e} > dp_{u_e} + w_e` holds, we set :math:`par_{v_e}` to :math:`u_e`. :math:`par_u` effectively represents the previous vertex in the optimal path from :math:`sc` to :math:`u`. To obtain the path from :math:`sc` to :math:`des`, we maintain a variable :math:`nw` and until :math:`nw \neq sc`, we set :math:`nw` to :math:`par_{nw}` and prepend :math:`nw` to the current obtained path. To prove that we will definitely reach the vertex :math:`sc` and that the obtained path is optimal, we assume an array :math:`lst`. :math:`lst_u` equals the number of the last stage in which :math:`dp_u` was changed. We know :math:`lst_u > lst_{par_u}` holds. (Proof is left to the reader) Therefore, every time we set :math:`nw` to :math:`par_{nw}`, :math:`lst_{nw}` decreases, so we do not loop and will definitely reach :math:`sc`. The path length will also be equal to :math:`dp_{des}` because at each step, if we add an edge with weight :math:`w` to the path, the value of :math:`dp_{nw}` decreases exactly by :math:`w`. Since :math:`dp_{sc} = 0`, the path length will be exactly equal to :math:`dp_{des}`. .. _negative-cycle: Negative Cycle -------------- You might wonder, if it's not guaranteed whether the graph has a negative cycle or not, how can we determine if a negative cycle exists? (Assuming we have optimized memory usage). First, suppose instead of :math:`|V(G)| - 1` iterations, we run the algorithm for :math:`|V(G)|` iterations. We call an iteration of the algorithm "good" if the value of at least one cell in the :math:`dp` array changes. We know that if iteration :math:`i` is not good, then subsequent iterations will also not be good. (If no values change, subsequent iterations will behave exactly like iteration :math:`i`, and no cells will change). Now, if there is no negative cycle, based on previous reasoning, iteration :math:`|V(G)|` will definitely not be good. (All cells will have reached their final values in the previous iteration and will not change). However, if a negative cycle exists, we know the :math:`dp` values of its vertices will never stabilize. The negative cycle itself can be traversed multiple times, causing the :math:`dp` values of the cycle's vertices to approach :math:`-\infty`. We also know that if an iteration is not good, then all values have stabilized. From these arguments, it follows that if a negative cycle exists, all iterations will be good. Thus, we conclude: If there is no negative cycle, iteration :math:`|V(G)|` will not be good, and if a negative cycle exists, iteration :math:`|V(G)|` will definitely be good. Therefore, to check for the existence of a negative cycle, it suffices to check whether iteration :math:`|V(G)|` is good. To find the negative cycle itself, similar to the case without negative cycles, maintain an auxiliary array called :math:`par`. Starting from a vertex whose value changed in iteration :math:`|V(G)|`, trace back the optimal path from :math:`sc` — the negative cycle will necessarily lie along this path. (This part can be proven based on the reasoning in this section).