BFS Algorithm Common Patterns and Code Template
Note
Now all the plugins has supported English. I'm still improving the website...
This article will resolve
LeetCode | Difficulty |
---|---|
752. Open the Lock | 🟠 |
773. Sliding Puzzle | 🔴 |
Prerequisite Knowledge
Before reading this article, you should first study:
I have emphasized multiple times that algorithms like DFS, backtracking, and BFS essentially abstract a specific problem into a tree structure and then traverse this tree to conduct brute-force enumeration. Thus, the code for these enumeration algorithms is fundamentally tree traversal code.
To clarify the causal relationships here:
The essence of DFS/backtracking algorithms is to recursively traverse an enumeration tree (multi-way tree), which is derived from the recursive traversal of binary trees. Therefore, I say that the essence of DFS/backtracking algorithms is the recursive traversal of binary trees.
The essence of BFS algorithms is to traverse a graph. As you will see, the framework for BFS algorithms is the code for traversing graph nodes in DFS/BFS Traversal of Graph Structures.
In fact, graph traversal algorithms are essentially multi-way tree traversal algorithms with an added visited
array to prevent infinite loops; multi-way tree traversal algorithms are derived from binary tree traversal algorithms. Therefore, I say the essence of BFS algorithms is level-order traversal of binary trees.
Why is the BFS algorithm often used to solve shortest path problems? I explained this in detail using the example of finding the minimum depth of a binary tree in Recursive/Level Order Traversal of Binary Trees.
In essence, what we call the shortest path can be likened to problems of finding the minimum depth of a binary tree (finding the leaf node closest to the root). Recursive traversal requires visiting all nodes of the tree to find the target node, whereas level-order traversal can achieve this without visiting all nodes, making it suitable for solving shortest path problems.
Is this explanation clear enough?
So, before reading this article, make sure you have learned Recursive/Level Order Traversal of Binary Trees, Recursive/Level Order Traversal of Multi-way Trees, and DFS/BFS Traversal of Graph Structures. Understanding these basic data structure traversal algorithms is crucial as it makes other algorithms easier to grasp.
The focus of this article is to teach you how to abstract and transform specific algorithm problems, then apply the BFS algorithm framework to solve them.
In real interview questions, you are generally not asked to directly traverse standard data structures like trees/graphs. Instead, you are given a specific scenario and need to abstract it into a standard graph/tree structure, then use the BFS algorithm to enumerate the solution.
For example, in a maze game, you are asked to calculate the minimum number of steps to reach the exit. If the maze includes teleportation portals that instantly transport you to another location, what is the minimum number of steps then?
Or, given two words, you are required to transform one into the other through a series of replacements, deletions, or insertions of characters. How many operations are needed at minimum?
Another example is a matching game where two blocks can be eliminated only if they have the same pattern and the shortest connecting line between them has no more than two turns. When you play such a game and click on two coordinates, how does the game determine the number of turns in the shortest connecting line?
These examples may initially seem unrelated to tree/graph structures, but with a bit of abstraction, they are indeed traversals of tree/graph structures. It becomes straightforward and monotonous.
Let's use a few examples to illustrate the BFS framework, and you will find these types of problems become much easier to solve.
1. Algorithm Framework
The framework for BFS (Breadth-First Search) is essentially the same as the BFS traversal of graph structures provided in Graph Traversal, and there are three ways to write it.
For practical BFS algorithm problems, the first method is the simplest but has significant limitations and is rarely used. The second method is the most commonly used, as it can solve most medium-level BFS algorithm problems. The third method is slightly more complex but offers the highest flexibility and may be used in more challenging BFS problems. In the next chapter, BFS Algorithm Exercises, there will be some more difficult problems employing the third method, which you can try on your own.
The examples in this article are of medium difficulty, so the solutions provided here will follow the second method:
// BFS traversal of the graph starting from s, and record the steps
// When the target node is reached, return the number of steps
int bfs(int s, int target) {
boolean[] visited = new boolean[graph.size()];
Queue<Integer> q = new LinkedList<>();
q.offer(s);
visited[s] = true;
// record the number of steps taken from s to the current node
int step = 0;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
int cur = q.poll();
System.out.println("visit " + cur + " at step " + step);
// determine if the target point is reached
if (cur == target) {
return step;
}
// add the neighbors to the queue to search around
for (int to : neighborsOf(cur)) {
if (!visited[to]) {
q.offer(to);
visited[to] = true;
}
}
}
step++;
}
// If we reach here, it means the target node was not found in the graph
return -1;
}
// BFS traversal of the graph starting from s, and record the steps
// When the target node is reached, return the number of steps
int bfs(const Graph& graph, int s, int target) {
vector<bool> visited(graph.size(), false);
queue<int> q;
q.push(s);
visited[s] = true;
// record the number of steps taken from s to the current node taken
int step = 0;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
int cur = q.front();
q.pop();
cout << "visit " << cur << " at step " << step << endl;
// determine if the target point is reached
if (cur == target) {
return step;
}
// add the neighbors to the queue to search around
for (int to : neighborsOf(cur)) {
if (!visited[to]) {
q.push(to);
visited[to] = true;
}
}
}
step++;
}
// If we reach here, it means the target node was not found in the graph
return -1;
}
# BFS traversal of the graph starting from s, and record the steps
def bfs(graph, s):
visited = [False] * len(graph)
q = deque([s])
visited[s] = True
# record the number of steps taken from s to the current node
step = 0
while q:
sz = len(q)
for i in range(sz):
cur = q.popleft()
print(f"visit {cur} at step {step}")
# determine if the target point is reached
if cur == target:
return step
# add neighboring nodes to the queue, and spread the search outward
for to in neighborsOf(cur):
if not visited[to]:
q.append(to)
visited[to] = True
step += 1
# If we reach here, it means the target node was not found in the graph
return -1
// BFS traversal of the graph starting from s, and record the steps
// When the target node is reached, return the number of steps
func bfs(graph Graph, s int, target int) int {
visited := make([]bool, graph.size())
q := []int{s}
visited[s] = true
// record the number of steps taken from s to the current node in the traversal
step := 0
for len(q) > 0 {
sz := len(q)
for i := 0; i < sz; i++ {
cur := q[0]
q = q[1:]
fmt.Printf("visit %d at step %d\n", cur, step)
// determine if the target point is reached
if cur == target {
return step
}
// add the neighbors to the queue to search around
for _, to := range neighborsOf(cur) {
if !visited[to] {
q = append(q, to)
visited[to] = true
}
}
}
step++
}
// If we reach here, it means the target node was not found in the graph
return -1
}
// BFS traversal of the graph starting from s, and record the steps
// When the target node is reached, return the number of steps
var bfs = function(graph, s, target) {
var visited = new Array(graph.size()).fill(false);
var q = [];
q.push(s);
visited[s] = true;
// record the number of steps taken from s to the current node in the traversal
var step = 0;
while (q.length !== 0) {
var sz = q.length;
for (var i = 0; i < sz; i++) {
var cur = q.shift();
// visit the current node
console.log("visit " + cur + " at step " + step);
// determine if the target point is reached
if (cur === target) {
return step;
}
// add the neighbors to the queue to search around
var neighbors = neighborsOf(cur);
for (var i = 0; i < neighbors.length; i++) {
var to = neighbors[i];
if (!visited[to]) {
q.push(to);
visited[to] = true;
}
}
}
step++;
}
// If we reach here, it means the target node was not found in the graph
return -1;
}
The code framework above is almost directly copied from DFS/BFS Traversal of Graph Structures, with the addition of a target
parameter. When the traversal first reaches the target
, the algorithm stops immediately and returns the number of steps taken.
Let's look at how to use this framework with some specific examples.
2. 773. Sliding Puzzle
LeetCode Problem 773, "Sliding Puzzle," is a problem that can be solved using the BFS framework. The problem is described as follows:
You are given a 2x3 sliding puzzle represented by a 2x3 array board
. The puzzle consists of numbers 0 to 5, where the number 0 represents the empty space. You can move the numbers around, and you win the game when board
becomes [[1,2,3],[4,5,0]]
.
Write an algorithm to calculate the minimum number of moves required to win the game. If the game cannot be won, return -1.
For example, if the input 2D array is board = [[4,1,2],[5,0,3]]
, the algorithm should return 5:
If the input is board = [[1,2,3],[5,4,0]]
, the algorithm should return -1, because in this configuration, winning the game is impossible.
I find this problem quite interesting, as it reminds me of similar puzzle games from childhood, such as the "Huarong Road" puzzle:
You need to move the blocks and find a way to move Cao Cao from the starting position to the exit at the bottom.
The "Huarong Road" puzzle is likely more difficult than this problem because, in this LeetCode problem, each block can be considered the same size, whereas in "Huarong Road," the blocks vary in size.
Returning to this problem, how can we abstract it into a tree/graph structure and solve it using the BFS algorithm framework?
In fact, the initial state of the board can be considered as the starting point:
[[2,4,1],
[5,0,3]]
Our ultimate goal is to transform the chessboard into this state:
[[1,2,3],
[4,5,0]]
This can be considered as the endpoint.
Now, doesn't this problem become a graph problem? Essentially, the question is asking for the shortest path from the start to the endpoint.
Who are the neighboring nodes of the starting point? By swapping the number 0 with the numbers above, below, left, and right, you actually get the four neighboring nodes of the start point (since the board size in this problem is 2x3, the actual neighboring nodes within the index boundaries will be less than four):
By analogy, each of these four neighboring nodes also has its own four neighboring nodes, forming a graph structure.
Starting from the initial point, I use the BFS algorithm to traverse this graph. The number of steps taken to reach the endpoint for the first time is the answer.
The pseudocode is as follows:
int bfs(int[][] board, int[][] target) {
Queue<int[][]> q = new LinkedList<>();
HashSet visited = new HashSet<>();
// add the start point to the queue
q.offer(board);
visited.add(board);
int step = 0;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
int[][] cur = q.poll();
// determine if the end point is reached
if (cur == target) {
return step;
}
// add the neighbors of the current node to the queue
for (int[][] neighbor : getNeighbors(cur)) {
if (!visited.contains(neighbor)) {
q.offer(neighbor);
visited.add(neighbor);
}
}
}
step++;
}
return -1;
}
List<int[][]> getNeighbors(int[][] board) {
// swap the number 0 with the numbers above, below, left, and right, to get 4 neighbors
}
For this problem, the abstracted graph structure may contain cycles, so we need a visited
array to record nodes that have already been traversed to avoid infinite loops caused by cycles.
For example, if I start from the node [[2,4,1],[5,0,3]]
, moving the number 0 to the right results in a new node [[2,4,1],[5,3,0]]
. However, in this new node, 0 can also move back to the left, returning to [[2,4,1],[5,0,3]]
, which creates a cycle. Thus, we need a visited
hash set to record nodes that have been visited, preventing infinite loops due to cycles.
Another issue is that in this problem, the board
is a two-dimensional array. As discussed in Hash Table/Hash Set Basics, mutable data structures like two-dimensional arrays cannot be directly added to a hash set.
Therefore, we need to use some techniques to transform the two-dimensional array into an immutable type before storing it in a hash set. A common solution is to serialize the two-dimensional array into a string, which can then be stored in the hash set.
The tricky part is that the two-dimensional array has concepts of "up, down, left, and right," so how do we exchange the number 0 with adjacent numbers once compressed into a one-dimensional string?
For this problem, since the input array size is always 2 x 3, we can manually write out this mapping:
// Record the adjacent indices of one-dimensional strings
int[][] neighbor = new int[][]{
{1, 3},
{0, 4, 2},
{1, 5},
{0, 4},
{3, 1, 5},
{4, 2}
};
// Record the adjacent indices of one-dimensional strings
int neighbor[6][3] = {
{1, 3},
{0, 4, 2},
{1, 5},
{0, 4},
{3, 1, 5},
{4, 2}
};
# Record the adjacent indices of a one-dimensional string
neighbor = [
[1, 3],
[0, 4, 2],
[1, 5],
[0, 4],
[3, 1, 5],
[4, 2]
]
// Record adjacent indices of one-dimensional strings
neighbor := [][]int{
{1, 3},
{0, 4, 2},
{1, 5},
{0, 4},
{3, 1, 5},
{4, 2},
}
// Record the adjacent indices of one-dimensional strings
var neighbor = [
[1, 3],
[0, 4, 2],
[1, 5],
[0, 4],
[3, 1, 5],
[4, 2]
];
The meaning of this mapping is that in a one-dimensional string, the index i
has adjacent indices in the two-dimensional array as neighbor[i]
:
What if it is an m x n
two-dimensional array?
For an m x n
two-dimensional array, manually writing its one-dimensional index mapping is not realistic; it needs to be generated through code.
Observing the image above, you can see that if a certain element e
in the two-dimensional array has an index i
in the one-dimensional array, then the indices of the elements adjacent to e
in the one-dimensional array are i - 1
and i + 1
for left and right, and i - n
and i + n
for up and down, where n
is the number of columns in the two-dimensional array.
Thus, for an m x n
two-dimensional array, we can write a function to generate its neighbor
index mapping:
int[][] generateNeighborMapping(int m, int n) {
int[][] neighbor = new int[m * n][];
for (int i = 0; i < m * n; i++) {
List<Integer> neighbors = new ArrayList<>();
// if it is not the first column, it has a left neighbor
if (i % n != 0) neighbors.add(i - 1);
// if it is not the last column, it has a right neighbor
if (i % n != n - 1) neighbors.add(i + 1);
// if it is not the first row, it has an upper neighbor
if (i - n >= 0) neighbors.add(i - n);
// if it is not the last row, it has a lower neighbor
if (i + n < m * n) neighbors.add(i + n);
// Java language feature, convert List type to int[] array
neighbor[i] = neighbors.stream().mapToInt(Integer::intValue).toArray();
}
return neighbor;
}
vector<vector<int>> generateNeighborMapping(int m, int n) {
vector<vector<int>> neighbor(m * n);
for (int i = 0; i < m * n; i++) {
vector<int> neighbors;
// if it's not the first column, it has a left neighbor
if (i % n != 0) neighbors.push_back(i - 1);
// if it's not the last column, it has a right neighbor
if (i % n != n - 1) neighbors.push_back(i + 1);
// if it's not the first row, it has an upper neighbor
if (i - n >= 0) neighbors.push_back(i - n);
// if it's not the last row, it has a lower neighbor
if (i + n < m * n) neighbors.push_back(i + n);
neighbor[i] = neighbors;
}
return neighbor;
}
def generateNeighborMapping(m: int, n: int) -> List[List[int]]:
neighbor = [[] for _ in range(m * n)]
for i in range(m * n):
neighbors = []
# if not the first column, it has a left neighbor
if i % n != 0: neighbors.append(i - 1)
# if not the last column, it has a right neighbor
if i % n != n - 1: neighbors.append(i + 1)
# if not the first row, it has an upper neighbor
if i - n >= 0: neighbors.append(i - n)
# if not the last row, it has a lower neighbor
if i + n < m * n: neighbors.append(i + n)
neighbor[i] = neighbors
return neighbor
func generateNeighborMapping(m, n int) [][]int {
neighbor := make([][]int, m*n)
for i := 0; i < m*n; i++ {
neighbors := make([]int, 0)
// if it's not the first column, it has a left neighbor
if i % n != 0 {
neighbors = append(neighbors, i-1)
}
// if it's not the last column, it has a right neighbor
if i % n != n-1 {
neighbors = append(neighbors, i+1)
}
// if it's not the first row, it has an upper neighbor
if i-n >= 0 {
neighbors = append(neighbors, i-n)
}
// if it's not the last row, it has a lower neighbor
if i+n < m*n {
neighbors = append(neighbors, i+n)
}
neighbor[i] = neighbors
}
return neighbor
}
function generateNeighborMapping(m, n) {
const neighbor = new Array(m * n).fill(0).map(() => []);
for (let i = 0; i < m * n; i++) {
const neighbors = [];
// if not in the first column, has a left neighbor
if (i % n !== 0) neighbors.push(i - 1);
// if not in the last column, has a right neighbor
if (i % n !== n - 1) neighbors.push(i + 1);
// if not in the first row, has an upper neighbor
if (i - n >= 0) neighbors.push(i - n);
// if not in the last row, has a lower neighbor
if (i + n < m * n) neighbors.push(i + n);
neighbor[i] = neighbors;
}
return neighbor;
}
In this way, regardless of where the number 0 is located, you can use this index mapping to obtain its adjacent index for swapping. Below is the complete code implementation:
class Solution {
public int slidingPuzzle(int[][] board) {
String target = "123450";
// convert the 2x3 array to a string as the starting point of BFS
String start = "";
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
start = start + board[i][j];
}
}
// ***** BFS algorithm framework starts *****
Queue<String> q = new LinkedList<>();
HashSet<String> visited = new HashSet<>();
// start BFS search from the starting point
q.offer(start);
visited.add(start);
int step = 0;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
String cur = q.poll();
// determine if the target state is reached
if (target.equals(cur)) {
return step;
}
// swap number 0 with its adjacent numbers
for (String neighborBoard : getNeighbors(cur)) {
// prevent from going back
if (!visited.contains(neighborBoard)) {
q.offer(neighborBoard);
visited.add(neighborBoard);
}
}
}
step++;
}
// ***** BFS algorithm framework ends *****
return -1;
}
private List<String> getNeighbors(String board) {
// record the adjacent indices of the one-dimensional string
int[][] mapping = new int[][]{
{1, 3},
{0, 4, 2},
{1, 5},
{0, 4},
{3, 1, 5},
{4, 2}
};
int idx = board.indexOf('0');
List<String> neighbors = new ArrayList<>();
for (int adj : mapping[idx]) {
String new_board = swap(board.toCharArray(), adj, idx);
neighbors.add(new_board);
}
return neighbors;
}
private String swap(char[] chars, int i, int j) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
return new String(chars);
}
}
class Solution {
public:
int slidingPuzzle(vector<vector<int>>& board) {
string target = "123450";
// convert the 2x3 array to a string as the starting point of BFS
string start = "";
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
start = start + to_string(board[i][j]);
}
}
// ***** BFS algorithm framework starts *****
queue<string> q;
unordered_set<string> visited;
// start BFS search from the starting point
q.push(start);
visited.insert(start);
int step = 0;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
string cur = q.front();
q.pop();
// determine if the target state is reached
if (target == cur) {
return step;
}
// swap number 0 with its adjacent numbers
for (string neighborBoard : getNeighbors(cur)) {
// prevent from going back
if (!visited.count(neighborBoard)) {
q.push(neighborBoard);
visited.insert(neighborBoard);
}
}
}
step++;
}
// ***** BFS algorithm framework ends *****
return -1;
}
vector<string> getNeighbors(string board) {
// record the adjacent indices of the one-dimensional string
vector<vector<int>> mapping = {
{1, 3},
{0, 4, 2},
{1, 5},
{0, 4},
{3, 1, 5},
{4, 2}
};
int idx = board.find('0');
vector<string> neighbors;
for (int adj : mapping[idx]) {
string new_board = swap(board, idx, adj);
neighbors.push_back(new_board);
}
return neighbors;
}
string swap(string board, int i, int j) {
char temp = board[i];
board[i] = board[j];
board[j] = temp;
return board;
}
};
from collections import deque
class Solution:
def slidingPuzzle(self, board):
target = "123450"
# convert the 2x3 array to a string as the starting point of BFS
start = ""
for i in range(len(board)):
for j in range(len(board[0])):
start += str(board[i][j])
# ***** BFS algorithm framework starts *****
q = deque()
visited = set()
# start BFS search from the starting point
q.append(start)
visited.add(start)
step = 0
while q:
# number of nodes in the current layer
sz = len(q)
for _ in range(sz):
cur = q.popleft()
# determine if the target state is reached
if cur == target:
return step
# swap number 0 with its adjacent numbers
for neighbor_board in self.getNeighbors(cur):
# prevent from going back
if neighbor_board not in visited:
q.append(neighbor_board)
visited.add(neighbor_board)
step += 1
# ***** BFS algorithm framework ends *****
return -1
def getNeighbors(self, board):
# record the adjacent indices of the one-dimensional string
mapping = [
[1, 3],
[0, 4, 2],
[1, 5],
[0, 4],
[3, 1, 5],
[4, 2]
]
idx = board.index('0')
neighbors = []
for adj in mapping[idx]:
new_board = self.swap(board, idx, adj)
neighbors.append(new_board)
return neighbors
def swap(self, board, i, j):
chars = list(board)
chars[i], chars[j] = chars[j], chars[i]
return ''.join(chars)
func slidingPuzzle(board [][]int) int {
target := "123450"
// convert the 2x3 array to a string as the starting point of BFS
start := ""
for i := 0; i < len(board); i++ {
for j := 0; j < len(board[0]); j++ {
start += string(rune(board[i][j] + '0'))
}
}
// ***** BFS algorithm framework starts *****
queue := []string{start}
visited := make(map[string]bool)
visited[start] = true
step := 0
for len(queue) > 0 {
sz := len(queue)
for i := 0; i < sz; i++ {
cur := queue[0]
queue = queue[1:]
// determine if the target state is reached
if cur == target {
return step
}
// swap number 0 with its adjacent numbers
for _, neighbor := range getNeighbors(cur) {
// prevent from going back
if !visited[neighbor] {
queue = append(queue, neighbor)
visited[neighbor] = true
}
}
}
step++
}
// ***** BFS algorithm framework ends *****
return -1
}
func getNeighbors(board string) []string {
// record the adjacent indices of the one-dimensional string
mapping := [][]int{
{1, 3},
{0, 4, 2},
{1, 5},
{0, 4},
{3, 1, 5},
{4, 2},
}
idx := strings.Index(board, "0")
neighbors := []string{}
for _, adj := range mapping[idx] {
newBoard := swap(board, idx, adj)
neighbors = append(neighbors, newBoard)
}
return neighbors
}
func swap(board string, i, j int) string {
chars := []rune(board)
chars[i], chars[j] = chars[j], chars[i]
return string(chars)
}
var slidingPuzzle = function(board) {
const target = "123450";
// convert the 2x3 array to a string as the starting point of BFS
let start = "";
for (let i = 0; i < board.length; i++) {
for (let j = 0; j < board[0].length; j++) {
start += board[i][j];
}
}
// ***** BFS algorithm framework starts *****
const queue = [start];
const visited = new Set();
visited.add(start);
let step = 0;
while (queue.length > 0) {
const sz = queue.length;
for (let i = 0; i < sz; i++) {
const cur = queue.shift();
// determine if the target state is reached
if (cur === target) {
return step;
}
// swap number 0 with its adjacent numbers
for (const neighbor of getNeighbors(cur)) {
// prevent from going back
if (!visited.has(neighbor)) {
queue.push(neighbor);
visited.add(neighbor);
}
}
}
step++;
}
// ***** BFS algorithm framework ends *****
return -1;
};
function getNeighbors(board) {
// record the adjacent indices of the one-dimensional string
const mapping = [
[1, 3],
[0, 4, 2],
[1, 5],
[0, 4],
[3, 1, 5],
[4, 2],
];
const idx = board.indexOf('0');
const neighbors = [];
for (const adj of mapping[idx]) {
const newBoard = swap(board, idx, adj);
neighbors.push(newBoard);
}
return neighbors;
}
function swap(board, i, j) {
const chars = board.split('');
[chars[i], chars[j]] = [chars[j], chars[i]];
return chars.join('');
}
This problem is solved. You will find that the BFS algorithm itself follows a fixed pattern. The challenge in this problem is actually converting the problem into a BFS brute-force model and then using a reasonable method to convert a multi-dimensional array into a string so that a hash set can record the visited nodes.
Let's look at another real-world scenario problem.
3. Minimum Turns to Unlock a Password Lock
Consider LeetCode Problem 752, "Open the Lock," which is quite interesting:
752. Open the Lock | LeetCode |
You have a lock in front of you with 4 circular wheels. Each wheel has 10 slots: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
. The wheels can rotate freely and wrap around: for example we can turn '9'
to be '0'
, or '0'
to be '9'
. Each move consists of turning one wheel one slot.
The lock initially starts at '0000'
, a string representing the state of the 4 wheels.
You are given a list of deadends
dead ends, meaning if the lock displays any of these codes, the wheels of the lock will stop turning and you will be unable to open it.
Given a target
representing the value of the wheels that will unlock the lock, return the minimum total number of turns required to open the lock, or -1 if it is impossible.
Example 1:
Input: deadends = ["0201","0101","0102","1212","2002"], target = "0202" Output: 6 Explanation: A sequence of valid moves would be "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202". Note that a sequence like "0000" -> "0001" -> "0002" -> "0102" -> "0202" would be invalid, because the wheels of the lock become stuck after the display becomes the dead end "0102".
Example 2:
Input: deadends = ["8888"], target = "0009" Output: 1 Explanation: We can turn the last wheel in reverse to move from "0000" -> "0009".
Example 3:
Input: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888" Output: -1 Explanation: We cannot reach the target without getting stuck.
Constraints:
1 <= deadends.length <= 500
deadends[i].length == 4
target.length == 4
- target will not be in the list
deadends
. target
anddeadends[i]
consist of digits only.
The function signature is as follows:
int openLock(String[] deadends, String target)
int openLock(vector<string>& deadends, string target)
def openLock(deadends: List[str], target: str) -> int
func openLock(deadends []string, target string) int {}
var openLock = function(deadends, target) {
}
The problem describes a common type of combination lock we encounter in daily life. If there are no constraints, calculating the minimum number of turns is straightforward. For example, to reach "1234"
, you simply turn each digit individually, resulting in a minimum of 1 + 2 + 3 + 4 = 10
turns.
However, the challenge arises when there are deadends
during the process of turning the combination lock. If you encounter deadends
, how can you handle them to minimize the total number of turns?
Avoid getting bogged down in details or trying to consider every specific situation. Remember, the essence of an algorithm is brute-force. We start directly from "0000"
and exhaustively enumerate all possible turning scenarios. Are we really afraid of not finding the minimum number of turns?
First, let's disregard all constraints, including deadends
and target
, and think about one question: If you were to design an algorithm to enumerate all possible combinations, how would you do it?
Starting from "0000"
, if you turn the lock once, how many possibilities are there? There are 4 positions, and each position can be turned up or down, resulting in possible combinations like "1000", "9000", "0100", "0900"...
, totaling 8 combinations.
Then, using these 8 combinations as a base, each one can be further turned to derive 8 more combinations, and so on...
Can you visualize that recursive tree in your mind? It should be an octary tree, with each node having 8 child nodes, branching downward.
The following pseudocode describes the above approach, traversing an octary tree:
// increment s[j] by one
String plusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '9')
ch[j] = '0';
else
ch[j] += 1;
return new String(ch);
}
// decrement s[j] by one
String minusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '0')
ch[j] = '9';
else
ch[j] -= 1;
return new String(ch);
}
// BFS framework to find the minimum number of moves
void BFS(String target) {
Queue<String> q = new LinkedList<>();
q.offer("0000");
int step = 0;
while (!q.isEmpty()) {
int sz = q.size();
// spread all nodes in the current queue to their neighbors
for (int i = 0; i < sz; i++) {
String cur = q.poll();
// determine if the end point is reached
if (cur.equals(target)) {
return step;
}
// a password can derive 8 neighboring passwords
for (String neighbor : getNeighbors(cur)) {
q.offer(neighbor);
}
}
// increase the step count here
step++;
}
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
List<String> getNeighbors(String s) {
List<String> neighbors = new ArrayList<>();
for (int i = 0; i < 4; i++) {
neighbors.add(plusOne(s, i));
neighbors.add(minusOne(s, i));
}
return neighbors;
}
// increment s[j] by one
string plusOne(string s, int j) {
char ch[s.length()];
strcpy(ch, s.c_str());
if (ch[j] == '9')
ch[j] = '0';
else
ch[j] += 1;
string res(ch);
return res;
}
// decrement s[i] by one
string minusOne(string s, int j) {
char ch[s.length()];
strcpy(ch, s.c_str());
if (ch[j] == '0')
ch[j] = '9';
else
ch[j] -= 1;
string res(ch);
return res;
}
// BFS framework to find the minimum number of moves
void BFS(string target) {
queue<string> q;
q.push("0000");
while (!q.empty()) {
int sz = q.size();
// spread all nodes in the current queue
for (int i = 0; i < sz; i++) {
string cur = q.front(); q.pop();
// determine if the end point is reached
if (cur == target) {
return step;
}
// a password can derive 8 neighboring passwords
for (string neighbor : getNeighbors(cur)) {
q.push(neighbor);
}
}
// increment the step count here
step++;
}
return -1;
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
vector<string> getNeighbors(string s) {
vector<string> neighbors;
for (int i = 0; i < 4; i++) {
neighbors.push_back(plusOne(s, i));
neighbors.push_back(minusOne(s, i));
}
return neighbors;
}
from typing import List
# increment s[j] by one
def plusOne(s: str, j: int) -> str:
ch = list(s)
if ch[j] == '9':
ch[j] = '0'
else:
ch[j] = chr(ord(ch[j]) + 1)
return ''.join(ch)
# decrement s[j] by one
def minusOne(s: str, j: int) -> str:
ch = list(s)
if ch[j] == '0':
ch[j] = '9'
else:
ch[j] = chr(ord(ch[j]) - 1)
return ''.join(ch)
# BFS framework to find the minimum number of moves
def BFS(target: str) -> int:
q = ['0000']
while q:
sz = len(q)
# spread all nodes in the current queue to their surroundings
for _ in range(sz):
cur = q.pop(0)
# determine if the end point is reached
if cur == target:
return step
# add the neighbors of a node to the queue
for neighbor in getNeighbors(cur):
q.append(neighbor)
# increment the step count here
step += 1
return -1
# increment or decrement each digit of s by one, return 8 neighboring passwords
def getNeighbors(s: str) -> List[str]:
neighbors = []
for i in range(4):
neighbors.append(plusOne(s, i))
neighbors.append(minusOne(s, i))
return neighbors
// turn s[j] up by one
func plusOne(s string, j int) string {
ch := []byte(s)
if ch[j] == '9' {
ch[j] = '0'
} else {
ch[j] += 1
}
return string(ch)
}
// turn s[i] down by one
func minusOne(s string, j int) string {
ch := []byte(s)
if ch[j] == '0' {
ch[j] = '9'
} else {
ch[j] -= 1
}
return string(ch)
}
// BFS framework to find the minimum number of moves
func BFS(target string) int {
q := []string{"0000"}
step := 0
for len(q) > 0 {
sz := len(q)
// spread all nodes in the current queue to their surroundings
for i := 0; i < sz; i++ {
cur := q[0]
q = q[1:]
// determine if the end point is reached
if cur == target {
return step
}
// add the neighbors of a node to the queue
for _, neighbor := range getNeighbors(cur) {
q = append(q, neighbor)
}
}
// increment the step count here
step++
}
return -1
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
func getNeighbors(s string) []string {
neighbors := []string{}
for i := 0; i < 4; i++ {
neighbors = append(neighbors, plusOne(s, i))
neighbors = append(neighbors, minusOne(s, i))
}
return neighbors
}
// increment s[j] by one
var plusOne = function(s, j) {
let ch = s.split('');
if(ch[j] == '9') ch[j] = '0';
else ch[j] = String.fromCharCode(ch[j].charCodeAt(0) + 1);
return ch.join('');
};
// decrement s[i] by one
var minusOne = function(s, j) {
let ch = s.split('');
if(ch[j] == '0') ch[j] = '9';
else ch[j] = String.fromCharCode(ch[j].charCodeAt(0) - 1);
return ch.join('');
}
// BFS framework to find the minimum number of moves
var BFS = function(target) {
let q = ['0000'];
let step = 0;
while(q.length > 0) {
let sz = q.length;
// spread all nodes in the current queue to their surroundings
for(let i = 0; i < sz; i++) {
let cur = q.shift();
// determine if the end point is reached
if(cur == target) {
return step;
}
// add the neighbors of a node to the queue
for(let neighbor of getNeighbors(cur)) {
q.push(neighbor);
}
}
// increment the step count here
step++;
}
return -1;
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
function getNeighbors(s) {
let neighbors = [];
for(let i = 0; i < 4; i++) {
neighbors.push(plusOne(s, i));
neighbors.push(minusOne(s, i));
}
return neighbors;
}
This code can already enumerate all possible password combinations, but there are still some issues to address.
- It retraces steps. We can turn the dial from
"0000"
to"1000"
, but when"1000"
is dequeued, it can turn back to"0000"
, creating an infinite loop.
This problem is easy to solve. Essentially, it's about forming a loop. We can use a visited
set to record passwords that have already been enumerated. If a password is encountered again, it should not be added back to the queue.
- The
deadends
are not handled. Ideally, these "deadlock passwords" should not appear.
This issue is also easy to handle. Use an additional deadends
set to record these deadlock passwords, and whenever one is encountered, do not add it to the queue.
Alternatively, it can be even simpler by initializing the visited
set with the passwords from deadends
. This also achieves the desired outcome.
Below is the complete code implementation:
class Solution {
public int openLock(String[] deadends, String target) {
// record the deadends that need to be skipped
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
if (deads.contains("0000")) return -1;
// record the passwords that have been exhausted to prevent backtracking
Set<String> visited = new HashSet<>();
Queue<String> q = new LinkedList<>();
// start breadth-first search from the starting point
int step = 0;
q.offer("0000");
visited.add("0000");
while (!q.isEmpty()) {
int sz = q.size();
// spread all nodes in the current queue to their surroundings
for (int i = 0; i < sz; i++) {
String cur = q.poll();
// determine if the end is reached
if (cur.equals(target))
return step;
// add the valid adjacent nodes of a node to the queue
for (String neighbor : getNeighbors(cur)) {
if (!visited.contains(neighbor) && !deads.contains(neighbor)) {
q.offer(neighbor);
visited.add(neighbor);
}
}
}
// increment the step count here
step++;
}
// if the target password is not found after exhaustion, then it is not found
return -1;
}
// turn s[j] up once
String plusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '9')
ch[j] = '0';
else
ch[j] += 1;
return new String(ch);
}
// turn s[i] down once
String minusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '0')
ch[j] = '9';
else
ch[j] -= 1;
return new String(ch);
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
List<String> getNeighbors(String s) {
List<String> neighbors = new ArrayList<>();
for (int i = 0; i < 4; i++) {
neighbors.add(plusOne(s, i));
neighbors.add(minusOne(s, i));
}
return neighbors;
}
}
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
// record the deadends that need to be skipped
unordered_set<string> deads(deadends.begin(), deadends.end());
if (deads.count("0000")) return -1;
// record the passwords that have been exhausted to prevent backtracking
unordered_set<string> visited;
queue<string> q;
// start breadth-first search from the starting point
int step = 0;
q.push("0000");
visited.insert("0000");
while (!q.empty()) {
int sz = q.size();
// spread all nodes in the current queue to their surroundings
for (int i = 0; i < sz; i++) {
string cur = q.front();
q.pop();
// determine if the end is reached
if (cur == target)
return step;
// add the valid adjacent nodes of a node to the queue
for (string neighbor : getNeighbors(cur)) {
if (!visited.count(neighbor) && !deads.count(neighbor)) {
q.push(neighbor);
visited.insert(neighbor);
}
}
}
// increment the step count here
step++;
}
// if the target password is not found after exhaustion, then it is not found
return -1;
}
// turn s[j] up once
string plusOne(string s, int j) {
if (s[j] == '9')
s[j] = '0';
else
s[j] += 1;
return s;
}
// turn s[i] down once
string minusOne(string s, int j) {
if (s[j] == '0')
s[j] = '9';
else
s[j] -= 1;
return s;
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
vector<string> getNeighbors(string s) {
vector<string> neighbors;
for (int i = 0; i < 4; i++) {
neighbors.push_back(plusOne(s, i));
neighbors.push_back(minusOne(s, i));
}
return neighbors;
}
};
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
# record the deadends that need to be skipped
deads = set(deadends)
if "0000" in deads:
return -1
# record the passwords that have been exhausted to prevent backtracking
visited = set()
q = collections.deque()
# start breadth-first search from the starting point
step = 0
q.append("0000")
visited.add("0000")
while q:
sz = len(q)
# spread all nodes in the current queue to their surroundings
for _ in range(sz):
cur = q.popleft()
# determine if the end is reached
if cur == target:
return step
# add the valid adjacent nodes of a node to the queue
for neighbor in self.getNeighbors(cur):
if neighbor not in visited and neighbor not in deads:
q.append(neighbor)
visited.add(neighbor)
# increment the step count here
step += 1
# if the target password is not found after exhaustion, then it is not found
return -1
# turn s[j] up once
def plusOne(self, s: str, j: int) -> str:
ch = list(s)
if ch[j] == '9':
ch[j] = '0'
else:
ch[j] = chr(ord(ch[j]) + 1)
return ''.join(ch)
# turn s[i] down once
def minusOne(self, s: str, j: int) -> str:
ch = list(s)
if ch[j] == '0':
ch[j] = '9'
else:
ch[j] = chr(ord(ch[j]) - 1)
return ''.join(ch)
# increment or decrement each digit of s by one, return 8 neighboring passwords
def getNeighbors(self, s: str) -> List[str]:
neighbors = []
for i in range(4):
neighbors.append(self.plusOne(s, i))
neighbors.append(self.minusOne(s, i))
return neighbors
func openLock(deadends []string, target string) int {
// record the deadends that need to be skipped
deads := make(map[string]struct{})
for _, s := range deadends {
deads[s] = struct{}{}
}
if _, found := deads["0000"]; found {
return -1
}
// record the passwords that have been exhausted to prevent backtracking
visited := make(map[string]struct{})
q := make([]string, 0)
// start breadth-first search from the starting point
step := 0
q = append(q, "0000")
visited["0000"] = struct{}{}
for len(q) > 0 {
sz := len(q)
// spread all nodes in the current queue to their surroundings
for i := 0; i < sz; i++ {
cur := q[0]
q = q[1:]
// determine if the end is reached
if cur == target {
return step
}
// add the valid adjacent nodes of a node to the queue
for _, neighbor := range getNeighbors(cur) {
if _, found := visited[neighbor]; !found {
if _, dead := deads[neighbor]; !dead {
q = append(q, neighbor)
visited[neighbor] = struct{}{}
}
}
}
}
// increment the step count here
step++
}
// if the target password is not found after exhaustion, then it is not found
return -1
}
// turn s[j] up once
func plusOne(s string, j int) string {
ch := []rune(s)
if ch[j] == '9' {
ch[j] = '0'
} else {
ch[j]++
}
return string(ch)
}
// turn s[i] down once
func minusOne(s string, j int) string {
ch := []rune(s)
if ch[j] == '0' {
ch[j] = '9'
} else {
ch[j]--
}
return string(ch)
}
// increment or decrement each digit of s by one, return 8 neighboring passwords
func getNeighbors(s string) []string {
neighbors := make([]string, 0)
for i := 0; i < 4; i++ {
neighbors = append(neighbors, plusOne(s, i))
neighbors = append(neighbors, minusOne(s, i))
}
return neighbors
}
var openLock = function(deadends, target) {
// record the deadends that need to be skipped
let deads = new Set(deadends);
if (deads.has("0000")) return -1;
// record the passwords that have been exhausted to prevent backtracking
let visited = new Set();
let q = [];
// start breadth-first search from the starting point
let step = 0;
q.push("0000");
visited.add("0000");
while (q.length !== 0) {
let sz = q.length;
// spread all nodes in the current queue to their surroundings
for (let i = 0; i < sz; i++) {
let cur = q.shift();
// determine if the end is reached
if (cur === target)
return step;
// add the valid adjacent nodes of a node to the queue
for (let neighbor of getNeighbors(cur)) {
if (!visited.has(neighbor) && !deads.has(neighbor)) {
q.push(neighbor);
visited.add(neighbor);
}
}
}
// increment the step count here
step++;
}
// if the target password is not found after exhaustion, then it is not found
return -1;
};
// turn s[j] up once
var plusOne = function(s, j) {
let ch = s.split('');
if (ch[j] === '9')
ch[j] = '0';
else
ch[j] = (parseInt(ch[j]) + 1).toString();
return ch.join('');
};
// turn s[i] down once
var minusOne = function(s, j) {
let ch = s.split('');
if (ch[j] === '0')
ch[j] = '9';
else
ch[j] = (parseInt(ch[j]) - 1).toString();
return ch.join('');
};
// increment or decrement each digit of s by one, return 8 neighboring passwords
var getNeighbors = function(s) {
let neighbors = [];
for (let i = 0; i < 4; i++) {
neighbors.push(plusOne(s, i));
neighbors.push(minusOne(s, i));
}
return neighbors;
};
4. Bidirectional BFS Optimization
Now, let's introduce an optimization strategy for the BFS algorithm: Bidirectional BFS, which can enhance the efficiency of BFS search.
Consider this technique as supplementary reading. In most interview or test scenarios, the regular BFS algorithm suffices. However, if you encounter time limitations or probing questions from interviewers, you might consider whether bidirectional BFS optimization is needed.
Bidirectional BFS is derived from the standard BFS algorithm:
The traditional BFS framework spreads outwards from the starting point until it reaches the endpoint, while bidirectional BFS starts spreading from both the starting and ending points simultaneously, stopping when they intersect.
Why does this improve efficiency?
It's similar to having two people, A and B. Traditional BFS is like A setting off to find B while B remains stationary. Bidirectional BFS, however, has both A and B starting towards each other, making it quicker for them to meet.
In the tree structure illustrated, if the endpoint is at the bottom, following the traditional BFS strategy would mean searching through all nodes of the tree before finally reaching the target
. In contrast, bidirectional BFS finds the intersection after traversing only half the tree, thus determining the shortest path.
From a Big O notation perspective, both types of BFS could potentially traverse all nodes in the worst-case scenario, resulting in a theoretical time complexity of . However, in practice, bidirectional BFS tends to be faster.
Limitations of Bidirectional BFS
You must know the location of the endpoint to use bidirectional BFS for optimization.
In BFS algorithms, we definitely know the starting point. However, the endpoint may not be apparent at the beginning.
In problems like the lock puzzle or sliding puzzle, the endpoint is explicitly given, allowing for bidirectional BFS optimization.
Conversely, in problems like determining the minimum height of a binary tree, discussed in DFS/BFS Traversal of Binary Trees, the starting point is the root node, and the endpoint is the closest leaf node to the root. Since you don't know the exact location of the endpoint when the algorithm starts, bidirectional BFS cannot be utilized for optimization.
Let's take the lock puzzle as an example to see how to optimize the regular BFS algorithm into a bidirectional BFS algorithm. Let's dive into the code:
class Solution {
public int openLock(String[] deadends, String target) {
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
// base case
if (deads.contains("0000")) return -1;
if (target.equals("0000")) return 0;
// Use a set instead of a queue to quickly determine if an element exists
Set<String> q1 = new HashSet<>();
Set<String> q2 = new HashSet<>();
Set<String> visited = new HashSet<>();
int step = 0;
q1.add("0000");
visited.add("0000");
q2.add(target);
visited.add(target);
while (!q1.isEmpty() && !q2.isEmpty()) {
// Increment the step count here
step++;
// The hash set cannot be modified during
// traversal, so use newQ1 to store the
Set<String> newQ1 = new HashSet<>();
// Get the neighbors of all nodes in q1
for (String cur : q1) {
// Add an unvisited neighboring node of a node to the set
for (String neighbor : getNeighbors(cur)) {
// Determine if the end point is reached
if (q2.contains(neighbor)) {
return step;
}
if (!visited.contains(neighbor) && !deads.contains(neighbor)) {
newQ1.add(neighbor);
visited.add(neighbor);
}
}
}
// newQ1 stores the neighbors of q1
q1 = newQ1;
// Because each BFS spreads q1, the set with fewer elements is used as q1
if (q1.size() > q2.size()) {
Set<String> temp = q1;
q1 = q2;
q2 = temp;
}
}
return -1;
}
// Turn s[j] up once
String plusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '9')
ch[j] = '0';
else
ch[j] += 1;
return new String(ch);
}
// Turn s[i] down once
String minusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '0')
ch[j] = '9';
else
ch[j] -= 1;
return new String(ch);
}
List<String> getNeighbors(String s) {
List<String> neighbors = new ArrayList<>();
for (int i = 0; i < 4; i++) {
neighbors.add(plusOne(s, i));
neighbors.add(minusOne(s, i));
}
return neighbors;
}
}
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
unordered_set<string> deads(deadends.begin(), deadends.end());
// base case
if (deads.count("0000")) return -1;
if (target == "0000") return 0;
// Use a set instead of a queue to quickly determine if an element exists
unordered_set<string> q1;
unordered_set<string> q2;
unordered_set<string> visited;
int step = 0;
q1.insert("0000");
visited.insert("0000");
q2.insert(target);
visited.insert(target);
while (!q1.empty() && !q2.empty()) {
// Increment the step count here
step++;
// The hash set cannot be modified during
// traversal, so use newQ1 to store the
unordered_set<string> newQ1;
// Get the neighbors of all nodes in q1
for (const string& cur : q1) {
// Add an unvisited neighboring node of a node to the set
for (const string& neighbor : getNeighbors(cur)) {
// Determine if the end point is reached
if (q2.count(neighbor)) {
return step;
}
if (!visited.count(neighbor) && !deads.count(neighbor)) {
newQ1.insert(neighbor);
visited.insert(neighbor);
}
}
}
// newQ1 stores the neighbors of q1
q1 = newQ1;
// Because each BFS spreads q1, the set with fewer elements is used as q1
if (q1.size() > q2.size()) {
swap(q1, q2);
}
}
return -1;
}
// Turn s[j] up once
string plusOne(string s, int j) {
if (s[j] == '9')
s[j] = '0';
else
s[j] += 1;
return s;
}
// Turn s[i] down once
string minusOne(string s, int j) {
if (s[j] == '0')
s[j] = '9';
else
s[j] -= 1;
return s;
}
vector<string> getNeighbors(string s) {
vector<string> neighbors;
for (int i = 0; i < 4; i++) {
neighbors.push_back(plusOne(s, i));
neighbors.push_back(minusOne(s, i));
}
return neighbors;
}
};
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
deads = set(deadends)
# base case
if "0000" in deads: return -1
if target == "0000": return 0
# Use a set instead of a queue to quickly determine if an element exists
q1 = set()
q2 = set()
visited = set()
step = 0
q1.add("0000")
visited.add("0000")
q2.add(target)
visited.add(target)
while q1 and q2:
# Increment the step count here
step += 1
# The hash set cannot be modified during
# traversal, so use newQ1 to store the
newQ1 = set()
# Get the neighbors of all nodes in q1
for cur in q1:
# Add an unvisited neighboring node of a node to the set
for neighbor in self.getNeighbors(cur):
# Determine if the end point is reached
if neighbor in q2:
return step
if neighbor not in visited and neighbor not in deads:
newQ1.add(neighbor)
visited.add(neighbor)
# newQ1 stores the neighbors of q1
q1 = newQ1
# Because each BFS spreads q1, the set with fewer elements is used as q1
if len(q1) > len(q2):
q1, q2 = q2, q1
return -1
# Turn s[j] up once
def plusOne(self, s: str, j: int) -> str:
ch = list(s)
if ch[j] == '9':
ch[j] = '0'
else:
ch[j] = chr(ord(ch[j]) + 1)
return ''.join(ch)
# Turn s[i] down once
def minusOne(self, s: str, j: int) -> str:
ch = list(s)
if ch[j] == '0':
ch[j] = '9'
else:
ch[j] = chr(ord(ch[j]) - 1)
return ''.join(ch)
def getNeighbors(self, s: str) -> List[str]:
neighbors = []
for i in range(4):
neighbors.append(self.plusOne(s, i))
neighbors.append(self.minusOne(s, i))
return neighbors
func openLock(deadends []string, target string) int {
deads := make(map[string]struct{})
for _, s := range deadends {
deads[s] = struct{}{}
}
// base case
if _, found := deads["0000"]; found {
return -1
}
if target == "0000" {
return 0
}
// Use a set instead of a queue to quickly determine if an element exists
q1 := make(map[string]struct{})
q2 := make(map[string]struct{})
visited := make(map[string]struct{})
step := 0
q1["0000"] = struct{}{}
visited["0000"] = struct{}{}
q2[target] = struct{}{}
visited[target] = struct{}{}
for len(q1) != 0 && len(q2) != 0 {
// Increment the step count here
step++
// The hash set cannot be modified during
// traversal, so use newQ1 to store the
newQ1 := make(map[string]struct{})
// Get the neighbors of all nodes in q1
for cur := range q1 {
// Add an unvisited neighboring node of a node to the set
for _, neighbor := range getNeighbors(cur) {
// Determine if the end point is reached
if _, found := q2[neighbor]; found {
return step
}
if _, found := visited[neighbor]; !found {
if _, found := deads[neighbor]; !found {
newQ1[neighbor] = struct{}{}
visited[neighbor] = struct{}{}
}
}
}
}
// newQ1 stores the neighbors of q1
q1 = newQ1
// Because each BFS spreads q1, the set with fewer elements is used as q1
if len(q1) > len(q2) {
q1, q2 = q2, q1
}
}
return -1
}
// Turn s[j] up once
func plusOne(s string, j int) string {
ch := []rune(s)
if ch[j] == '9' {
ch[j] = '0'
} else {
ch[j]++
}
return string(ch)
}
// Turn s[i] down once
func minusOne(s string, j int) string {
ch := []rune(s)
if ch[j] == '0' {
ch[j] = '9'
} else {
ch[j]--
}
return string(ch)
}
func getNeighbors(s string) []string {
neighbors := []string{}
for i := 0; i < 4; i++ {
neighbors = append(neighbors, plusOne(s, i))
neighbors = append(neighbors, minusOne(s, i))
}
return neighbors
}
var openLock = function(deadends, target) {
let deads = new Set(deadends);
// base case
if (deads.has("0000")) return -1;
if (target === "0000") return 0;
// Use a set instead of a queue to quickly determine if an element exists
let q1 = new Set();
let q2 = new Set();
let visited = new Set();
let step = 0;
q1.add("0000");
visited.add("0000");
q2.add(target);
visited.add(target);
while (q1.size && q2.size) {
// Increment the step count here
step++;
// The hash set cannot be modified during
// traversal, so use newQ1 to store the
let newQ1 = new Set();
// Get the neighbors of all nodes in q1
for (let cur of q1) {
// Add an unvisited neighboring node of a node to the set
for (let neighbor of getNeighbors(cur)) {
// Determine if the end point is reached
if (q2.has(neighbor)) {
return step;
}
if (!visited.has(neighbor) && !deads.has(neighbor)) {
newQ1.add(neighbor);
visited.add(neighbor);
}
}
}
// newQ1 stores the neighbors of q1
q1 = newQ1;
// Because each BFS spreads q1, the set with fewer elements is used as q1
if (q1.size > q2.size) {
let temp = q1;
q1 = q2;
q2 = temp;
}
}
return -1;
};
// Turn s[j] up once
function plusOne(s, j) {
let ch = s.split('');
if (ch[j] === '9')
ch[j] = '0';
else
ch[j] = (parseInt(ch[j]) + 1).toString();
return ch.join('');
}
// Turn s[i] down once
function minusOne(s, j) {
let ch = s.split('');
if (ch[j] === '0')
ch[j] = '9';
else
ch[j] = (parseInt(ch[j]) - 1).toString();
return ch.join('');
}
function getNeighbors(s) {
let neighbors = [];
for (let i = 0; i < 4; i++) {
neighbors.push(plusOne(s, i));
neighbors.push(minusOne(s, i));
}
return neighbors;
}
Bidirectional BFS still follows the BFS algorithm framework but with a few key differences:
Instead of using a queue to store elements, it uses a hash set to quickly check if two sets intersect.
The position of
return step
is adjusted. In bidirectional BFS, it's not just about reaching the end point, but rather checking if the two sets intersect. Therefore, this check is done as soon as neighbor nodes are computed.There is an optimization point: always keep
q1
as the set with fewer elements, which can reduce the number of searches to some extent.
According to BFS logic, the more elements in the queue (set), the more elements will be in the new queue (set) after expanding neighbor nodes. In bidirectional BFS, if we always choose the smaller set for expansion, the space growth rate will be slower and efficiency higher.
That said, whether using traditional BFS or bidirectional BFS, with or without optimization, the time complexity from a Big O perspective remains the same. Bidirectional BFS is an advanced technique that runs relatively faster, but mastering it is not crucial.
The most important thing is to memorize the general BFS framework and use it proficiently. The following sections are a set of BFS exercises; try to solve them independently.
Related Problems
You can install my Chrome extension then open the link.