Skip to content

Breadth-First Search

We now consider the following revised version of the climbing stairs problem.

Climbing stairs with minimum moves

You are climbing a staircase with n steps. At each step, you can climb any number of steps from a given list steps (e.g., [1, 3, 5]). Find the minimum number of moves required to reach the top.

Instead of finding all possible paths to climb \(n\) steps, we now want to find the minimum number of moves to reach the top. This is the shortest path problem, which can be solved more efficiently using breadth-first search.

Breadth-first search

As shown in the figure above, breadth-first search explores all nodes at the present depth level before moving on to nodes at the next depth level. This level-order traversal ensures that the first time we reach the target node, we have found the shortest path. In contrast, depth-first search explores nodes in a single direction, which may lead to longer paths.

Implementation

Breadth-first traversal is usually implemented with the help of a "queue". The queue follows the "first in, first out" rule, while breadth-first traversal follows the "layer-by-layer progression" rule, the underlying ideas of the two are consistent. The generic idea to implement breadth-first search is as follows:

from collections import deque

def bfs_template(start_state, is_goal_state: callable, get_next_states: callable, compute_result: callable):
    # Initialize queue
    result = []
    queue = deque([(start_state, result)]) 
    while queue:
        # Initialize visited set
        visited = []
        # Get the current state
        current_state, current_result = queue.popleft() 

        # Skip if current state is already visited
        if current_state in visited:
            continue
        visited.append(current_state)

        # Check if current state is the goal
        if is_goal_state(current_state):
            return current_result

        # Explore next possible states
        for next_state in get_next_states(current_state):
            # Compute new result based on current result and next state
            next_result = compute_result(current_result, next_state)
            queue.append((next_state, next_result))

    return None # No goal state found

We now apply this template to solve the minimum moves to climb stairs problem.

from collections import deque

def min_moves_to_climb(n, steps):
    # BFS Initialization
    queue = deque([(0, 0)])  # (current position, number of moves)
    visited = []  # To avoid revisiting the same step

    while queue:
        position, moves = queue.popleft()

        # If we reach the top, return the number of moves
        if position == n:
            return moves

        # Explore all possible moves
        for step in steps:
            next_position = position + step
            if next_position <= n and next_position not in visited:
                visited.append(next_position)
                queue.append((next_position, moves + 1))

    return -1  # If reaching the top is not possible

Complexity analysis

Let \(n\) be the number of all possible states. - Time complexity is \(O(n)\): All nodes are visited once, taking \(O(n)\) time. - Space complexity is \(O(n)\): In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue can contain at most \(O(n)\) nodes simultaneously, occupying space.