Problem Statement
The Single-Source Shortest Paths (SSSP) problem is generally defined as the following:
Given a graph
G
and a source vertexs
, what are the shortest paths froms
to every other vertex inG
?
Example: Given a city (graph) with junctions (vertices) and roads (edges),
what is the shortest path from our current location at junction s
to every
other junction in the city?
The solution to the SSSP problem varies wildly depending on the state of the edges in graph. Are the edges weighted? unweighted? Are there any negative weights? Are there any cycles?
Because of this, we’ll go over a couple of different algorithms that solve the SSSP problem.
SSSP on Unweighted Graphs
General Idea | \(O(V + E)\)
If our graph’s edges are unweighted (or have a constant weighted), the solution
of the SSSP problem will revolve around using the well known
Breath-First Search
algorithm, also known as BFS
.
A couple of things to note:
BFS
, by nature, visits every vertex of a graph from a sources
on a layer by layer basis.- In an unweighted graph, the distance between any two neighbouring vertices connected with an edge is 1 unit.
With these two facts in mind, we can safely say that the layer count of any vertex in our graph
is the length of the shortest path from our source s
to that vertex.
Should our edges have a constant weight C
, we simple multiply C
with our layer count to get the correct lenght.
To actually get the path itself, we’ll have to store and reconstruct the BFS
spanning tree.
This can be done by using a vector<int>
. this vector
will be used to store
the parent of each vertex v
. Then, with a simple recursive function, we can
recursively reconstruct the shortest path using this vector
.
Note: This algorithm is suitable for graphs with V, E <= 10M to be able to pass the TL (Time Limit)
Code
C++11 code below.
SSSP on Weighted (Positive) Graphs
General Idea | \(O((V+E)LogV)\)
Since the edges are weighted, we cannot use BFS since it mainly relies on counting direct edges, not taking into account shorter but undirect paths.
The proper algorithm to solve this problem is Dijkstra’s Shortest Path Algorithm.
Dijkstra’s algorithm can be implemented using many different data structures (sets, priority queues, heaps) and has multiple variations and uses (A* algorithm, Johnson’s algorithm).
For our purposes, we’ll focus on the priority queue implementation, mainly due to its brevity and because priority queues are built-into C++.
To start things off, we’ll use a min priority queue. Meaning lower (shorter) values are at the top of the priority queue. We do this to be able to greedly pick the shortest path. If we were to find the longest path, we’d use a max priority queue instead.
We’ll first create a priority queue of pairs, priority_queue< pair<int, int> > pq
.
Each pair will hold our destination vertex u
and the distance from our source vertex s
to u
(distance from s
to u
, u
).
We’ll also have a vector<int> dist
to hold the minimum distance from s
to u
. This
is where our solution will be after we run the algorithm.
Initially, pq
will contain our base case: (0, s
), which is true for the source vertex.
If we had multiple source vertices (we could start from any of them), we’d add them all pq
before starting our main loop.
Our main loop will consist of the following:
- Greedly pop off the
front
ofpq
. For each unvisitedu
that appears infront
, it is guaranteed thatfront
will also hold the shortest distance froms
tou
. So, if only wanted the shortest distance froms
to vertex 8, we’d break our loop oncefront
is 8. - Attempt to relax all of
u
’s neighbours. Assuming one ofu
’s neighbours isv
, The relax operation revolves around updatingdist
by checking if the distance froms
tov
viau
is shorter than the currently known distance froms
tov
. - If we manage to relax one of
u
neighbours, we’ll add the newer and shorter (distance, vertex) pair topq
A couple of things to note with this algorithm:
- Shorter distances will always appear at the top, thus is
u
has not been visited before, the extracted distance frompq
’sfront
is the minumum. - This algorithm does NOT work in the presence of negative edge cycles, since the total distance becomes lower each time the cycle is traversed, leading the algorithm to be stuck in an infite loop. In short, do not use this algorithm with negative-weight edges.
Code
C++11 code below.
SSSP on Weighted (Positive or Negative) Graphs
If our graph contains or even might contain negative-weight edges, it’s better to use the Bellman-Ford Algorithm because, again, using Dijkstra’s algorithm with negative weights might lead to a infinite loop due to the presence of negative cycles.
Bellman-Ford’s algorithm can even detect negative cycles and report their existence should it be necessary.
General Idea | \(O(VE)\)
The Bellman-Ford algorithm, like Dijkstra’s, revolves around the concept of relaxation. However, Dijkstra’s algorithm uses a priority queue to greedily select the closest (shortest in distance) vertex that has not been processed, and performs this relaxation process on all of its outgoing edges.
By contrast, the Bellman-Ford algorithm simply relaxes all the edges,
and it does this V - 1
times, where V
is the number of vertices.
The reason/proof of why this works can be found in this Wikipedia article.
The Bellman-Ford algorithm can also be used to detect the presence of cycles by simply
running one more iteration after the main V - 1
times. If any changes are
made during this extra iteration then a cycle exists.
It is also worth noting that while in the worst case this algorithm runs in
\(O(VE)\), we can usually terminate it much earlier if an iteration of
the main loop (where we loop V - 1
times) ends without making any changes as
subsequent iterations will not have any further effect.
Code
C++11 code below.