Skip to main content

5.4 Breadth-First Search

Breadth-First Search (BFS) differs from Depth-First Search in that it traverses level by level, so it requires a first-in, first-out queue instead of the last-in, first-out stack used in DFS. Since BFS processes nodes by levels, it explores nodes in a "broad" manner and is often used for solving shortest path problems. In Python, we can use collections.deque to implement the queue used in C++.

   1
/ \
2 3
/
4

It’s important to note that both DFS and BFS can solve reachability problems, such as determining if one node is reachable from another. Many people prefer DFS for such problems because it can be quickly implemented using recursion. However, in practical software engineering, recursive implementations are rarely used because they are harder to understand and may cause stack overflow. Since iterative DFS (using a stack) and BFS (using a queue) have similar implementations, the choice between them depends on the specific requirements of the task. Additionally, if custom traversal priorities are needed, we can use priority queues, which will be discussed in the data structures section.

1091. Shortest Path in Binary Matrix

Problem Description

Given a 2D binary matrix where 1 represents obstacles and 0 represents open paths, each position is connected to its eight neighbors. Find the shortest path from the top-left corner to the bottom-right corner. If there is no valid path, return -1.

Input and Output Example

Input is a 2D integer array, and the output is an integer representing the shortest distance.

Input:
[[0,0,1],
[1,1,0],
[1,1,0]]
Output: 4

The shortest path is to move right first, then turn and move downward.

Solution Explanation

Using a queue, we can intuitively apply Breadth-First Search (BFS) to determine the minimum number of layers to expand, i.e., the shortest path to the destination. Be careful not to revisit previously searched positions.

int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
if (grid[0][0] == 1) {
return -1;
}
int m = grid.size(), n = grid[0].size();
int dist = 0, count;
queue<pair<int, int>> q;
q.push({0, 0});
grid[0][0] = -1; // -1 indicates visited
count = q.size();
while (count > 0) {
++dist;
while (count--) {
auto [r, c] = q.front();
q.pop();
if (r == m - 1 && c == n - 1) {
return dist;
}
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
if (dx == 0 && dy == 0) {
continue;
}
int x = r + dx, y = c + dy;
if (x < 0 || y < 0 || x >= m || y >= n || grid[x][y] != 0) {
continue;
}
grid[x][y] = -1;
q.push({x, y});
}
}
}
count = q.size();
}
return -1;
}

934. Shortest Bridge

Problem Description

Given a 2D binary grid where 1 represents land and 0 represents water, each position is connected to its four neighbors. There are exactly two islands in the grid. Find the minimum number of water cells to convert into land to connect the two islands.

Input and Output Example

Input is a 2D integer grid, and the output is a non-negative integer representing the number of water cells to fill.

Input:
[[1,1,1,1,1],
[1,0,0,0,1],
[1,0,1,0,1],
[1,0,0,0,1],
[1,1,1,1,1]]
Output: 1

Solution Explanation

This problem can be solved by finding the shortest distance between the two islands. First, identify one of the islands using any search method, then use Breadth-First Search (BFS) to find the shortest distance to the other island. Below is the implementation using Depth-First Search (DFS) to find the first island.

vector<int> direction{-1, 0, 1, 0, -1};
// Auxiliary function

void dfs(queue<pair<int, int>>& points, vector<vector<int>>& grid, int i,
int j) {
int m = grid.size(), n = grid[0].size();
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == 2) {
return;
}
if (grid[i][j] == 0) {
points.push({i, j});
return;
}
grid[i][j] = 2;
for (int k = 0; k < 4; ++k) {
dfs(points, grid, i + direction[k], j + direction[k + 1]);
}
}

// Main function
int shortestBridge(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> points;
// DFS to find the first island, and change all 1s to 2
bool flipped = false;
for (int i = 0; i < m && !flipped; ++i) {
for (int j = 0; j < n && !flipped; ++j) {
if (grid[i][j] == 1) {
dfs(points, grid, i, j);
flipped = true;
}
}
}
// BFS to find the second island, and change all 0s encountered to 2
int level = 0;
while (!points.empty()) {
++level;
int n_points = points.size();
while (n_points--) {
auto [r, c] = points.front();
points.pop();
grid[r][c] = 2;
for (int k = 0; k < 4; ++k) {
int x = r + direction[k], y = c + direction[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n) {
if (grid[x][y] == 2) {
continue;
}
if (grid[x][y] == 1) {
return level;
}
grid[x][y] = 2;
points.push({x, y});
}
}
}
}
return 0;
}

126. Word Ladder II

Problem Description

Given a start string, an end string, and a word list, determine if you can transform the start string into the end string by changing one character at a time, where every intermediate string must exist in the word list. If possible, output all transformation sequences with the minimum number of changes.

Input and Output Example

Input consists of two strings and a word list, and the output is a 2D string array representing all transformation sequences.

Input: beginWord = "hit", endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
Output:
[["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]]

Solution Explanation

We can think of the start string, the end string, and all strings in the word list as nodes. If two strings differ by exactly one character, they are connected. Since the problem requires outputting all transformation sequences with the minimum number of changes, we can use breadth-first search (BFS) to compute the shortest path from the start node to the end node.

We also use a small optimization: instead of performing BFS only from the start node until the end node is found, we perform BFS from both the start node and the end node simultaneously. Each time, we extend the smaller frontier to minimize the total number of nodes explored. For example, if the shortest distance is 4, performing BFS from one end would explore up to 1+2+4+8+16=311 + 2 + 4 + 8 + 16 = 31 nodes, while performing BFS from both ends for two levels would explore only 2×(1+2+4)=142 × (1 + 2 + 4) = 14 nodes.

After completing the search, we use backtracking to reconstruct all possible paths.

This problem is somewhat complex and requires careful thought and implementation. LeetCode's time constraints for this problem are very strict, so even the official solutions may sometimes time out. Multiple submissions may be necessary.

// Auxiliary function
void backtracking(const string &src, const string &dst,
unordered_map<string, vector<string>> &next_words,
vector<string> &path, vector<vector<string>> &ladder) {
if (src == dst) {
ladder.push_back(path);
return;
}
if (!next_words.contains(src)) {
return;
}
for (const auto &w : next_words[src]) {
path.push_back(w); // Modify the current node state
backtracking(w, dst, next_words, path, ladder); // Recursively process child nodes
path.pop_back(); // Revert the current node state
}
}

// Main function
vector<vector<string>> findLadders(string beginWord, string endWord,
vector<string> &wordList) {
vector<vector<string>> ladder;
// Use a hash set to store the dictionary for quick lookups.
unordered_set<string> word_dict;
for (const auto &w : wordList) {
word_dict.insert(w);
}
if (!word_dict.contains(endWord)) {
return ladder;
}
word_dict.erase(beginWord);
word_dict.erase(endWord);
// Create two queues for bidirectional BFS from beginWord and endWord,
// expanding the smaller queue each time.
unordered_set<string> q_small{beginWord}, q_large{endWord};
unordered_map<string, vector<string>> next_words;
bool reversed_path = false, found_path = false;
while (!q_small.empty()) {
unordered_set<string> q;
for (const auto &w : q_small) {
string s = w;
for (int i = 0; i < s.size(); ++i) {
for (int j = 0; j < 26; ++j) {
s[i] = j + 'a';
if (q_large.contains(s)) {
reversed_path ? next_words[s].push_back(w)
: next_words[w].push_back(s);
found_path = true;
}
if (word_dict.contains(s)) {
reversed_path ? next_words[s].push_back(w)
: next_words[w].push_back(s);
q.insert(s);
}
}
s[i] = w[i];
}
}
if (found_path) {
break;
}
// Avoid revisiting nodes and infinite loops.
for (const auto &w : q) {
word_dict.erase(w);
}
// Update the two queues and maintain size relationship.
if (q.size() <= q_large.size()) {
q_small = q;
} else {
reversed_path = !reversed_path;
q_small = q_large;
q_large = q;
}
}
if (found_path) {
vector<string> path{beginWord};
backtracking(beginWord, endWord, next_words, path, ladder);
}
return ladder;
}