1. Overview
In this tutorial, we’ll discuss the problem of obtaining the path in the uniform cost search algorithm.
First, we’ll define the problem and provide an example that explains it.
Then we’ll discuss two different approaches to solve this problem.
2. Defining the Problem
Suppose we have a graph, , that contains
nodes. In addition, we have
edges that connect these nodes. Each of these edges has a weight associated with it, representing the cost to use this edge. We’re also given two numbers,
and
, that represent the source node and the destination node, respectively.
Our task is to find the path from the source node to the destination
using the uniform cost search algorithm. Let’s look at an example for better understanding.
Suppose we have the following graph and we’re given and
:
To go from node to node
we have four possible paths:
-
, with cost equal to
.
-
, with cost equal to
.
-
, with cost equal to
.
-
, with cost equal to
.
When we apply the uniform cost search algorithm, we will get the minimal cost to go from the source node to the destination node
. In this example, the cost will be equal to
; t****herefore, the path that the algorithm will detect between nodes
and
is
.
3. Naive Approach
3.1. Main Idea
The main idea here is to store all the nodes that are on the path from the source node to the destination. For every node, we’ll store two values:
-
: which represents the cost of the path from the source node to the current one
-
: which represents a list of nodes that lies on the path with the minimum cost from the source node to the current one
During uniform cost search, when we reach a child node with a smaller cost than the previous one, we’ll update the value for the child node to the new cost. In addition, we’ll change the
value to be equal to the
value of the
node after appending the child node to it.
In the end, the value of the node
will have a list representing the path that goes from the source node
to the destination
with the minimum cost value. Also, the cost value of
will be equal to the minimum cost to go from
to
.
3.2. Implementation
Let’s take a look at the implementation:
algorithm NaiveApproach(V, G, S, D):
// INPUT
// V = Number of nodes in the graph
// G = The graph stored as an adjacency list
// S = The index of the source node
// D = The index of the destination node
// OUTPUT
// Returns the path between S and D with minimum cost
cost <- an array with all elements initialized to infinity
path <- an empty set
priorityQueue <- an empty queue
priorityQueue.add(S)
cost[S] <- 0
path[S] <- [S]
while not priorityQueue.empty():
current <- priorityQueue.front()
priorityQueue.pop()
for child in G[current]:
if cost[child] > cost[current] + weight(current, child):
priorityQueue.add(child)
cost[child] <- cost[current] + weight(current, child)
path[child] <- path[current]
path[child].append(child)
return path[D]
Initially, we declare the and the
arrays. The
value for all nodes is infinity, except the source node, which is equal to
(the length of the path from a node to itself equals
). Furthermore, the
value for all nodes is an empty list, except the source node, which is equal to a list containing the source node
(a path from a node to itself contains just the node itself).
We also declare an empty priority queue to store the explored nodes sorted in the ascending order according to their cost value.
Next we perform multiple steps as long as the priority queue isn’t empty. In each step, we take the node positioned in the front of the queue and iterate over its children. For each child node, we check whether it has a
value greater than the
value of the
node plus the weight of the edge that connects the
node with the
node.
If so, we add it to the priority queue, and we update its value to be equal to the
of the current node plus the weight of the edge that connects it with the
node. We also update its
value to be equal to the
value of the
node, since the path that goes from the source to the child node passing through the
node has a smaller cost. Then we append the
node to its
value.
Note that we need to copy the list using the deep copy technique. We want to take a copy of the list, including its element, and not just copy the reference to the list itself.
Finally, we return , which stores a list of nodes that represent the smallest cost path that goes from the source
to the destination
.
3.3. Complexity
The time complexity here is a little different from the uniform cost search complexity. The uniform cost search algorithm has a complexity equal to , where
is the number of nodes and
is the number of edges. However, since in this approach we store a list containing the path of each node, the complexity for this approach will be
. This is because each list can have a size equal to
in the worst case. Also, we might need to update each list
times.
The space complexity is in the worst-case scenario, since each time we store all the nodes from the source node to the current node.
4. Optimized Approach
4.1. Main Idea
We’ll apply the same concepts from the previous approach, but instead of storing all the nodes that lie on the path from the source node to the current node, we’ll just store the node that comes before the
node in the path. In the previous approach, the
of each node was similar to its parent after appending the current node. Thus, we can only store the
node instead of copying the complete list into the
node.
After finishing the uniform cost search algorithm, we’ll append the destination node to the path and move to the
node and add it to the beginning of the path. We’ll keep following the
nodes until we reach the source node
. At that point, we’ll have the path that goes from node
to node
.
4.2. Implementation
Let’s take a look at the implementation:
algorithm OptimizedApproach(V, G, S, D):
// INPUT
// V = Number of nodes in the graph
// G = The graph stored as an adjacency list
// S = The index of the source node
// D = The index of the destination node
// OUTPUT
// Returns the path from S to D with minimum cost using parent tracing
cost <- an array with all elements initialized to infinity
parent <- an array with all elements initialized to null
priorityQueue <- {}
priorityQueue.add(S)
cost[S] <- 0
while not priorityQueue.empty():
current <- priorityQueue.front()
priorityQueue.pop()
for child in G[current]:
if cost[child] > cost[current] + weight(current, child):
priorityQueue.add(child)
cost[child] <- cost[current] + weight(current, child)
parent[child] <- current
path <- []
node <- D
while node != null:
path.add_front(node)
node <- parent[node]
return path
Initially, we declare the same array and priority queue from the previous approach. We also declare the
array that will store the node that should come before the current node in the path.
Next we’ll perform the same steps from the previous approach; however, when we update the value of the
node, we’ll update the
value of the
node to be equal to the
node.
After finishing the uniform cost search, we declare the list that will store the source’s path to the destination. We also declare
, which is initially equal to the destination node. Then, while
isn’t equal to
, we add the
to the beginning of the
list and move to the
of
.
The reason we keep moving until we reach the value is that only the source
doesn’t have a parent. Furthermore, we add the node to the beginning of the list because we’re moving from the last node in the path
to the first node
. Since we’re moving backward, we add the new node at the beginning of the list.
Finally, we return , which stores a list of nodes that represent the path that goes from the source node
to the destination
.
4.3. Complexity
The time complexity here is , where
is the number of nodes and
is the number of edges. This is because we don’t copy the
list anymore; instead, we only set the
value.
The space complexity is since we just store a single value for each node.
5. Conclusion
In this article, we presented the problem of obtaining the path in the uniform cost search algorithm. We explained the main ideas behind two different approaches to solving this problem and worked through their implementation.