Breadth First Search and Single Source Shortest Path
A look at the breadth first search algorithm, and its application in graphs for finding single source shortest paths.
What is Breadth First Search?
Breadth First Search (BFS) is a method for searching tree and graph structures. It is also a traversal for technique for these two important abstract data types. BFS employs the opposite strategy from Depth First Search(DFS): where DFS will search down one branch as far is it can go before turning around and heading down another branch, BFS will check the first node of every branch in a "level", before dropping down and searching the next level. This is why we can guarantee BFSs "completeness". It's also why in a tree, BFS is sometimes referred to a "level order" traversal. BFS has been likened to a ring of fire burning equally outwards in all directions from the starting point. Its search pattern can also be thought of the as the concentric ripples spreading outward created by dropping a pebble in a pond.
Order Matters
When searching a collection, any collection, be it an array, tree, graph, or some other data structure, the ordering that the elements are arranged in and the order in which the elements are accessed are of critical importance. For example, if you're searching for a specific value in an array of integers then the Binary Search algorithm can be used to drastically cut down on the number of comparisons required for a search compared to the sequential search method - but only if the items are arranged in a sorted order. Like wise, if we're looking for a value in an implicitly infinite tree or graph, searching depth first may get lost exploring a branch leading AWAY from the vertex we're looking for, and thus never find the target we're searching for. Breadth First Search on the other hand suffers from no such drawback.
The Algorithm
Due to the nature of the Breadth First Search algorithm, while in the process of searching an unweighted graph it will also find the shortest path to whatever vertex it is we are looking for. The order which vertexes are accessed in BFS also means that it does NOT lend itself to a recursive implementation. There is a very simple queue based iterative form however. Where DFS utilizes a LIFO stack, BFS takes advantage of a FIFO queue.
Lets take a look at a pseudo code description for Breadth First Search before getting into the implementation details in C++.
Procedure: BreadthFirstSearch <- Graph, starting Vertex, finishing Vertex Let Q := a first in first out queue Let success := bool <- false add starting to Q mark starting as seen camefrom <- starting := starting while Q is not empty let current := first item in Q if current is target success := true for each child of current if seen child is false add child to Q seen child := true end while return success end procedure
Breadth First Search in C++
Were going to be using the adjacency representation of a graph shown in my article Representing Graphs with the C++ STL as the input graph for our breadth first search function, so if you haven't read that article yet or need a quick refresher, go have a look.
With that done its time to get down to business. As mentioned above, in order to perform our BFS we need a way to keep track of which vertexes we've already been to, and which ones we haven't seen yet. To do this we will use an std::map of vertex name -> boolean values. We're also going to keep track of which vertex we came from previously: this will allow us to rebuild our search path. For this we will also use an std::map.
The first think we need to do is declare our variables and containers:
void breadthFirstSearch(Graph& G, char start, char fin)
{
bool success = false;
char current = start;
char child, next;
std::map<char, bool> seen; //which vertexes have been visited
std::map<char, char> camefrom; //for rebuilding bath
std::queue<char> searchqueue; //search queue for BFS = FIFO
Next, we add start to the queue and mark it as seen:
searchqueue.push(current); //push initial vertex
seen[current] = true; //mark as visited.
After that, we proceed with the while loop as outlined in the algorithm pseudo code above:
while (!searchqueue.empty())
{
current = searchqueue.front(); //get the first item on the queue
searchqueue.pop(); //remove it from the queue
if (current == fin) //is this our search target?
{
success = true; //hooray
break; //we can stop looking
}
for (auto u : G.adjList[current]) //for every adjacent vertex of current
{
child = u.first;
if (seen[child] == false) //if we havent yet visited this vertex
{
searchqueue.push(child); //add it to the queue
seen[child] = true; //no need to see it twice.
camefrom[child] = current;
}
}
}
Now if we wanted to, we could just output whether success is true or not. We're going to go one step further and will output the traversal path through the graph that led to our successful search:
if (success)
{
cout<<"A path from "<<start<<" to "<<fin<<" was found.\n";
next = fin;
std::vector<char> path;
while (next != start)
{
path.push_back(next);
next = camefrom[next];
}
reverse(path.begin(), path.end());
cout<<"Path Length: "<<path.size()+1;
cout<<"\nPath: "<<start;
for (auto n : path)
{
cout<<" -> "<<n;
}
cout<<endl;
} else {
cout<<"\nNo such path Exists. ("<<start<<" -> "<<fin<<")\n";
}
}
To test our algorithm, we use the following driver program to build a graph and pass it to breadthFirstSearch():
int main(int argc, char *argv[])
{
Graph G(6);
G.addEdge('A', 'B', 0);
G.addEdge('A', 'C', 0);
G.addEdge('B', 'C', 0);
G.addEdge('C', 'E', 0);
G.addEdge('D', 'E', 0);
G.addEdge('D', 'F', 0);
G.addEdge('E', 'F', 0);
G.showAdjList();
breadthFirstSearch(G, 'A', 'D');
return 0;
}
Output: A: B C B: A C C: A B E D: E F E: C D F F: D E A path from A to D was found. Path Length: 4 Path: A -> C -> E -> D
There you have it: Breadth First Search and Single Source Shortest Path!
As always, the full code for the example shown in this article is available on my github:
https://github.com/maxgoren/examples/blob/main/graphing-bfs.cpp
-
Pratt Parsing: Top-Down Operator Precedence
-
Generating P-Code by AST Traversal
-
Ternary Search Tries: String Specific Ordered Symbol Tables
-
Digital Search Trees
-
Lossless Compression Part III: Huffman Coding
-
Lossless Compression Part II: The LZ77 Algorithm
-
Lossless Compression Part I: Working with Bits in a Byte Oriented World
-
Bottom Up AVL Tree: The OG Self-Balancing Binary Search Tree
-
A Different Take on Merge Sort: Binary Search Trees?
-
Deleting Entries From Open-Address Hash tables
Leave A Comment