Skip to content

Expert: advanced patterns and interviews · Graphs · topic 42 of 61 · 40 min

Breadth-first search (BFS)

Explanation

Breadth-first search explores a graph in layers: first the start, then everything one edge away, then everything two edges away. It uses a queue and a visited set, runs in O(V + E), and gives the shortest path when every edge counts the same, such as the fewest metro stops. Grids and "many starting points" problems are BFS too.

The idea: ripples in water

Drop a stone in a pond and the ripples spread in rings: first the ring closest to the stone, then the next ring, and so on. Breadth-first search (BFS) explores a graph the same way. It visits the start, then every vertex one edge away, then every vertex two edges away. Nothing at distance 3 is visited before everything at distance 2.

Think of it like this: Think of news spreading in a college hostel. You tell your friends (layer 1). Each of them tells their friends who have not heard yet (layer 2). The layer in which a person first hears the news is how many "hops" away from you they are.

The queue and the visited set

BFS keeps a queue: a line where items join at the back and leave from the front (first in, first out). In Python use collections.deque, whose popleft() is O(1). A plain list with pop(0) shifts every item, which is O(n) per pop.

Graphs can have cycles, so BFS also keeps a visited set: the vertices it has already put in the queue. Mark a vertex as visited WHEN YOU ADD IT to the queue, not when you take it out. Otherwise the same vertex can be added many times by different neighbours before it is processed.

python
from collections import deque

def bfs_dist(adj, start):
    dist = {start: 0}
    queue = deque([start])
    while queue:
        u = queue.popleft()
        for v in adj[u]:
            if v not in dist:          # dist doubles as the visited set
                dist[v] = dist[u] + 1
                queue.append(v)
    return dist

adj = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]}
print(bfs_dist(adj, 0))   # Output: {0: 0, 1: 1, 2: 1, 3: 2, 4: 3}
A dictionary of distances works as the visited set and records the layer of every vertex.

Every vertex enters the queue at most once and every edge is looked at once from each end, so BFS takes O(V + E) time with an adjacency list. The queue and the visited set take O(V) extra space.

Why BFS finds shortest paths

Because BFS works layer by layer, the first time it reaches a vertex is along a path with the fewest possible edges. So in an unweighted graph, dist[v] is the shortest distance from the start. "Fewest metro stops from Andheri to Churchgate" or "fewest connections between two people" are BFS questions. When edges have different weights (km, minutes, rupees), BFS is no longer enough; topic 5 covers Dijkstra.

Grids are graphs too

In a grid problem, each cell is a vertex and its neighbours are the cells up, down, left and right. You do not build an adjacency list; you compute neighbours on the fly with a list of four direction steps, checking that the new cell is inside the grid and not a wall. A grid with R rows and C columns has R × C vertices, so BFS costs O(R × C).

python
DIRS = [(1, 0), (-1, 0), (0, 1), (0, -1)]
for dr, dc in DIRS:
    nr, nc = r + dr, c + dc
    if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != "#":
        ...   # (nr, nc) is a neighbour you may visit
Check the bounds BEFORE indexing: in Python grid[-1] silently reads the last row instead of failing.

Multi-source BFS

Sometimes the spreading starts from many places at once. In the "rotting oranges" problem, every rotten orange makes its fresh neighbours rotten each minute. Put ALL the rotten oranges in the queue at minute 0, then run one BFS. Each layer is one minute. This is far faster than running a separate BFS from every source, and it answers "distance to the NEAREST source" for every cell.

To count layers, process the queue one layer at a time: for _ in range(len(queue)) pops exactly the vertices of the current layer, then you add 1 to the layer count.

Common mistakes

  • Marking visited when a vertex is popped instead of when it is pushed. The answer may still be right, but vertices get queued many times. Fix: mark on push.
  • Using list.pop(0) for the queue, which makes BFS O(V²) on big inputs. Fix: deque and popleft().
  • Forgetting the case start == target, where the answer is 0. Fix: check it first, or let dist[start] = 0 answer it.
  • Indexing grid[nr][nc] before checking 0 <= nr < rows. Negative indexes wrap around in Python, so the bug is silent. Fix: check bounds first.
  • Mixing up "number of moves" and "number of cells on the path" (they differ by 1). Fix: read the prompt and test on a 1 × 1 grid.
  • Using BFS on a graph with different edge weights. Fix: Dijkstra (topic 5).

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Breadth-first search (BFS) · 1/8

Breadth-first search

This lesson is about breadth-first search, the way to explore a graph in layers, like ripples in a pond.

Prefer a video by a teacher? Find free videos on “breadth first search bfs shortest path grid multi source explained python” on YouTube ↗

In real life: Nearest ambulance station for every block

An emergency-response team wants to know, for every block of a city map, how many blocks away the nearest ambulance station is. Running a BFS from every block would repeat a lot of work. Instead, put every station in the queue at distance 0 and run one multi-source BFS: each block is reached first from its nearest station. Walls (#) are blocks you cannot pass, such as a river or a closed road. Real dispatch systems use road networks and live traffic, but the layered idea is the same.

python
from collections import deque

city = ["S..#",
        "..#.",
        "...S"]
rows, cols = len(city), len(city[0])
dist = [[-1] * cols for _ in range(rows)]
queue = deque()
for r in range(rows):
    for c in range(cols):
        if city[r][c] == "S":
            dist[r][c] = 0
            queue.append((r, c))
while queue:
    r, c = queue.popleft()
    for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
        nr, nc = r + dr, c + dc
        if 0 <= nr < rows and 0 <= nc < cols and city[nr][nc] != "#" and dist[nr][nc] == -1:
            dist[nr][nc] = dist[r][c] + 1
            queue.append((nr, nc))
for row in dist:
    print(row)
Shows [0, 1, 2, -1], [1, 2, -1, 1] and [2, 2, 1, 0]: walls stay -1, every other block gets its distance to the nearest station.

Practice (0/4 done)

Task 1 · Code · Python

Write bfs_order(adj, start). adj is a dict mapping every vertex to the list of its neighbours (the graph may be directed and may have cycles). Return the list of vertices in the order BFS visits them from start, looking at neighbours in the order they are listed. Vertices that cannot be reached are not included. Example: bfs_order({0: [1, 2], 1: [3], 2: [3], 3: []}, 0) returns [0, 1, 2, 3].

Task 2 · Code · Python

Write shortest_path_len(n, edges, s, t) for an UNDIRECTED, unweighted graph with vertices 0 to n − 1 given as a list of pairs. Return the smallest number of edges on a path from s to t, 0 if s == t, and -1 if t cannot be reached. Use BFS. Example: shortest_path_len(5, [(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)], 0, 3) returns 2.

Task 3 · Code · Python

Write grid_shortest_path(grid). grid is a list of equal-length strings: "." is open and "#" is a wall. Moving one step up, down, left or right costs 1 move (no diagonals). Return the fewest moves from the top-left cell to the bottom-right cell, or -1 if that is impossible, if either corner is a wall, or if the grid is empty. A 1 × 1 open grid needs 0 moves.

Task 4 · Code · Python

Write oranges_rotting(grid). grid is a list of lists of ints: 0 is an empty cell, 1 a fresh orange and 2 a rotten orange. Every minute, each rotten orange makes the fresh oranges directly up, down, left and right of it rotten. Return the number of minutes until no fresh orange is left, 0 if there are no fresh oranges at the start, and -1 if some fresh orange can never rot. Use multi-source BFS. Example: [[2, 1, 1], [1, 1, 0], [0, 1, 1]] returns 4.

Quick quiz

Question 1

1.Why should BFS mark a vertex as visited when it is added to the queue?

Question 2

2.Which is the right queue for BFS in Python?

Question 3

3.What is the time of BFS on a graph stored as an adjacency list?

Question 4

4.In "rotting oranges", how does the BFS start?

Question 5

5.In Python, a grid BFS computes nr = -1 and reads grid[nr][nc] without a bounds check. What happens?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why should BFS mark a vertex as visited when it is added to the queue?

Answer: So the same vertex is not added to the queue many times

If you wait until it is popped, several neighbours can each add it before then.

Which is the right queue for BFS in Python?

Answer: collections.deque with popleft()

deque.popleft() is O(1); list.pop(0) shifts every item, O(n).

What is the time of BFS on a graph stored as an adjacency list?

Answer: O(V + E)

Each vertex is queued once and each edge is checked a constant number of times.

In "rotting oranges", how does the BFS start?

Answer: With every rotten orange in the queue at minute 0

Multi-source BFS: all sources share layer 0, so one pass gives each cell its distance to the nearest source.

In Python, a grid BFS computes nr = -1 and reads grid[nr][nc] without a bounds check. What happens?

Answer: It silently reads the last row

Negative indexes count from the end, so the bug hides. Check 0 <= nr < rows first.

Why can BFS give a wrong answer on a weighted graph?Related

Answer: It counts edges, not the total weight

BFS settles a vertex by the fewest edges, but a path with more, cheaper edges can cost less.

From Shortest paths: Dijkstra and friends

An undirected graph has 5 edges. What do all the degrees add up to?Related

Answer: 10

Every edge touches two vertices, so it adds 2 to the total: 2 × 5 = 10.

From Graphs: vertices, edges and how to store them

In word ladder, what are the vertices and the edges?Related

Answer: Words; two words that differ in exactly one letter

It is an implicit graph whose edges are generated by changing one letter.

From Graph problem patterns

Why use collections.deque instead of a list for BFS in Python?Related

Answer: popleft() is O(1), while list.pop(0) shifts every item, O(n)

Taking from the front of a list moves all other items; a deque is built for both ends.

From Traversals: DFS and BFS

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “Graphs” before its exam: Depth-first search (DFS), Topological sort, Shortest paths: Dijkstra and friends, Graph problem patterns.
  2. Module 9 exam: Graphs · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, Tries, union-find and bit manipulation. 61 topics left in the course before the final exam.

← Previous: Graphs: vertices, edges and how to store them