Data Structures & Algorithms (DSA) Module 11 – Graphs: modelling, traversal and connectivity
Implicit graphs, state-space search and 0-1 BFS
Find the fewest knight moves between two squares by searching a graph that is never stored, starting from the brute force and the signal that points to BFS.
What you will learn
- Model puzzles as graphs of states
- Search state spaces with BFS and bidirectional BFS
- Use 0-1 BFS when every edge costs 0 or 1
Before you start
What you’ll learn · Before you start
What you will learn
- Model puzzles as graphs of states
- Search state spaces with BFS and bidirectional BFS
- Use 0-1 BFS when every edge costs 0 or 1
Before you start
On this page
Many search problems have a graph that nobody writes down. In implicit graphs the vertices are states, such as a knight’s square or a puzzle’s tray, and a rule generates the edges from each state only when they are needed. State-space search runs an ordinary graph search over those states. Breadth-first search from the start finds the fewest moves when every move costs the same, and it reaches each state once. 0-1 BFS finds the cheapest route when moves cost either 0 or 1, with a double-ended queue in place of a heap. In every case, the number of states decides the cost. Below is a problem of this kind, the fewest knight moves, with its brute force and the signal to notice.
The problem: the fewest knight moves between two squares
The problem
A knight starts on square (r1, c1) of a square board with n rows and n columns, both counted from 0. Each move
jumps two squares in one direction and one square at right angles, and the knight may never leave the board. Return the
fewest moves that take it to (r2, c2), or −1 when no sequence of moves gets there. Limits: 1 ≤ n ≤ 1,000.
- n = 8, from
(0, 0)to(7, 7): 6 moves, corner to corner. - n = 8, from
(0, 0)to(1, 1): 4 moves. Two would do on an unbounded board, but both such routes leave this one. - n = 3, from
(0, 0)to(1, 1): −1, because no move ever lands on the centre of a 3 × 3 board.
Every square of a 5 × 5 board is a state; the number is its fewest moves from the corner
Text description of the diagram
The board has 5 rows and 5 columns, numbered from 0 at the top left, where the knight starts. Each square shows the fewest knight moves needed to reach it from there.
Row 0: 0 (the start), 3, 2, 3, 2. Row 1: 3, 4, 1, 2, 3. Row 2: 2, 1, 4, 3, 2. Row 3: 3, 2, 3, 2, 3. Row 4: 2, 3, 2, 3, 4.
Only two squares are one move away: row 1, column 2 and row 2, column 1. The square diagonally next to the start, row 1, column 1, needs 4 moves, as many as the far corner, because the moves that would reach it in two would leave the board.
The brute force: try every sequence of moves
Try every sequence of one move, then of two, and so on, until one ends on the target. That is right, but it never remembers a square, so it reaches the same squares again and again by different sequences:
# The brute force: try every sequence of knight moves, shortest sequences first (iterative deepening), and count the
# sequences it tries. It keeps no record of squares already reached, so it explores the same squares again and again.
MOVES = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)]
def fewest_by_trying(n, start, target):
"""Fewest moves, or -1; also the number of move sequences tried."""
tried = 0
def reaches(square, moves_left):
nonlocal tried
tried += 1
if square == target:
return True
if moves_left == 0:
return False
r, c = square
return any(reaches((r + dr, c + dc), moves_left - 1)
for dr, dc in MOVES if 0 <= r + dr < n and 0 <= c + dc < n)
for limit in range(n * n): # a shortest route never repeats a square: at most n*n - 1 moves
if reaches(start, limit):
return limit, tried
return -1, tried
import math
for n, start, target in [(8, (0, 0), (7, 7)), (8, (0, 0), (1, 1)), (3, (0, 0), (1, 1))]:
moves, tried = fewest_by_trying(n, start, target)
print(f"n = {n}, {start} -> {target}: answer {moves} after {tried:,} move sequences")
counts = []
for k in (3, 6, 9, 11):
moves, tried = fewest_by_trying(16, (0, 0), (k, k))
counts.append(tried)
print(f"n = 16, (0, 0) -> {(k, k)}: {moves} moves after {tried:,} move sequences")
rate = (counts[-1] / counts[-2]) ** 0.5 # the last two targets are 2 moves apart
print(f"each extra move multiplies the sequences by about {rate:.1f}")
print(f"n = 1,000, corner to corner, 666 moves: about 10^{round(666 * math.log10(rate))} sequences") Output
n = 8, (0, 0) -> (7, 7): answer 6 after 3,427 move sequences n = 8, (0, 0) -> (1, 1): answer 4 after 133 move sequences n = 3, (0, 0) -> (1, 1): answer -1 after 1,013 move sequences n = 16, (0, 0) -> (3, 3): 2 moves after 8 move sequences n = 16, (0, 0) -> (6, 6): 4 moves after 113 move sequences n = 16, (0, 0) -> (9, 9): 6 moves after 3,870 move sequences n = 16, (0, 0) -> (11, 11): 8 moves after 174,991 move sequences each extra move multiplies the sequences by about 6.7 n = 1,000, corner to corner, 666 moves: about 10^551 sequences
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each extra move multiplies the work by about 6.7, so 8 moves already cost 174,991 sequences, and the 666 moves from corner to corner of a 1,000 × 1,000 board would need about 10⁵⁵¹. An answer of −1 is worse: the search must try every sequence of up to n² − 1 moves before it can give up.
What to notice
The future of the knight depends only on its square, never on the route that brought it there. So each square is a state: a vertex of a graph with at most n² = 10⁶ vertices and 8 edges each, made on the fly. Implicit graphs of this size are small for a search that reaches every state at most once. Every move costs one and the question asks for the fewest moves, which is exactly what breadth-first search answers. This is state-space search in its simplest form, with a square as the whole state. Bidirectional BFS runs one search from the start and one from the target and stops where they meet. If some moves cost nothing and others cost one, 0-1 BFS keeps the same linear cost.
BFS over the squares, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why the first visit is the fewest moves: the layer invariant
- The code in five languages, with run records
- How many states, before any code
- Bidirectional BFS, measured
- 0-1 BFS: when a move can cost nothing
- Edge cases and four common bugs, reproduced
- Variants: blocked squares, many knights, route counts and costs of 2
- Exercise: fewest knight moves
- Fewest knight moves between two squares
- Check yourself
- A practice set with answers
- Interview questions
- In a state-space search, what must a state hold, and what goes wrong if it leaves something out?
- Your BFS for the fewest knight moves passes every test on boards up to 1,000 × 1,000. A new version must answer on boards of 100,000 × 100,000 within a second. What breaks first, and how do you still give the exact answer?
- A robot arm has 4 joints, each set to one of 36 angles. A move turns one joint by one notch either way, and 5 % of all settings make the arm hit its frame. How many states are there, what does BFS between two settings cost, and how much does searching from both ends save?
- A route planner for warehouse robots returns routes through the fewest cells, but operations wants the fewest turns, and turning around counts as two. Model the states and the costs, choose the search, and estimate its work and memory on a 2,000 × 2,000 grid.
- Key takeaways
7 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 24 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- collections.deque (Python Software Foundation)
- tracemalloc: trace memory allocations (Python Software Foundation)
- heapq: heap queue algorithm (Python Software Foundation)
- Dictionary of Algorithms and Data Structures: breadth-first search (National Institute of Standards and Technology (NIST))
- ACM/IEEE-CS/AAAI Computer Science Curricula 2023: Algorithmic Foundations (AL) (ACM, IEEE Computer Society and AAAI)
- Notes on the "15" puzzle (W. W. Johnson and W. E. Story, American Journal of Mathematics 2(4), 1879) (Johns Hopkins University Press (via JSTOR))
- A note on two problems in connexion with graphs (E. W. Dijkstra, Numerische Mathematik 1, 1959) (Springer)
- ECMAScript language specification: TypedArray objects (Ecma International (TC39))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress