Before any picture, agree on what a search is. Everything in this lecture is the same six-line loop with a different answer to one question.
You are handed a problem and told to “search” for a solution. Before you can write a single line, what three things do you have to be given?
Think about the wolf, the goat and the cabbage from last time. What did you need to know to even start? (The full formulation and state space, if you want it in front of you.)
A state — what a snapshot of the world looks like. The actions — what you can do in a state, and what state each one leads to. A goal test — how to recognise that you are done.
Nothing about how to search. That is the point: the loop below works for any problem you can describe this way, which is why it counts as artificial intelligence and not just as programming.
Here is the whole thing.
fringe ← { start }
while fringe is not empty:
current ← fringe.remove() ← the one decision
if current is a goal: return the path to it
for each child of current: fringe.add(child)
return no solution
Put the start on the fringe. While there is anything left to look at, take something
off, test it, and put its children on. That is breadth-first search. It is also depth-first
search, iterative deepening, uniform-cost search and bidirectional search. The only thing
that changes is which thing fringe.remove() hands back.
What is wrong with this loop? There are two things, and one of them will make it run forever on the very first maze we look at.
It never notices it has been somewhere before. In a grid you can step east then west and be back where you started; the loop will happily put that state on the fringe again, and again. The fix is a second data structure — a closed set of states already expanded — and a check against it before expanding. Loop with the check: graph search. Loop without it: tree search, fine only when the space really is a tree and you cannot cycle back.
It does not say what the fringe is. A queue? A stack? Something else? That is not an implementation detail. It is the entire lecture.
In every environment below the state is the cell the agent stands in, and the actions are the four compass moves. That is a choice, and it is not quite what the real MiniGrid robot does — it has a heading, and turning is an action. We will come back to what that costs at the very end; the first exercise of the homework is exactly that question.
The smallest environment MiniGrid ships: a 3×3 room. Red triangle is the agent, green square is the goal. Do this one by hand before pressing anything.
Say the fringe is a queue: the thing you remove is always the oldest thing on it. Starting from the agent's cell, which cells get expanded first? Group the nine cells by when they come out.
Which cells are exactly one move from the agent? Exactly two? Draw it.
The agent's cell first (distance 0). Then the two cells one move away. Then the three at distance two, the two at distance three, and finally the goal at distance four. Oldest-first means layer by layer: the queue empties out one whole distance before it starts the next.
The viewer opens on the distance layers so you can check your grouping. Then press Search and play. It expands 7 cells to reach a goal 4 steps away.
Pick an algorithm and step with ▶◀. The panel below the map is the fringe itself, drawn in the order it holds things, with what just came off and what just went on. That data structure is the entire difference between these six.
Step BFS and DFS each to their second pop and compare the two fringes. They hold the same cells. So what is actually different?
Which cell will each one remove next?
Only the end you take from. The queue removes the cell that has waited longest, so it finishes the near cells before starting the far ones. The stack removes the cell it just added, so it dives. Same cells, same loop, one line of code apart — and everything that follows in this lecture comes from that line.
That layer-by-layer order is not a coincidence of this map; it is what “oldest first” means. And it buys a guarantee: if there is a solution at all, there is a shallowest one, and because the queue finishes every layer before starting the next, it reaches that layer — and that solution — before any deeper one. This is breadth-first search, and what it promises is the path with the fewest steps. Hold on to the exact wording. It will matter.
Same loop. Swap the queue for a stack, so the thing you remove is the newest. Six-by-six interior, thirty-six cells.
The children of a cell are added north, east, south, west, and a stack hands back the last thing added. From the top-left corner, where does the search go first? Will the path it finds be worse than breadth-first's?
Follow it two or three moves by hand. Where is it heading?
North is a wall, so it goes east — and then keeps going east along the top row, then south down the right edge, straight into the goal. It expands 11 cells where breadth-first expands 34, and its path is 10 steps, the same as breadth-first's.
So on this room, newest-first did a third of the work for the same answer. Watch it, then read the next question before you decide it is better.
Try DFS, then DFS (west first). Nothing about the algorithm changed between them.
Now switch to DFS (west first): identical algorithm, the children just added in the order west, north, east, south. Before you play it — will the answer change?
It finds a path of 30 steps for a problem whose answer is 10, and it takes 31 expansions to do it. Nothing about the map or the loop changed. Only the order the children went onto the fringe — a choice the algorithm never told you it was making.
Breadth-first search gives the same 10-step answer in every order. That is what a guarantee is: it does not depend on getting lucky. Newest-first search, depth-first search, promises nothing about the quality of what it finds.
If breadth-first search is guaranteed to find the fewest-step path and depth-first search is not — and both of them, in the worst case, look at every reachable cell — why would anyone ever use depth-first search? Do not answer yet. We will earn the answer in a few pages.
MiniGrid's simplest crossing: one wall with one gap. Start top-left, goal bottom-right.
There is a wall in the way now. What has to change in the six-line loop to handle it?
Nothing. A wall is just a cell with no way in, so it never appears as anyone's child. The loop does not know what a wall is; it only knows children. That is why the same code searches a maze, a Rubik's cube and a social network: the problem supplies the children, the loop supplies the order.
What changes is the answer and the work: breadth-first now expands 41 cells for a 12-step path.
This viewer starts in the Undiscovered view: every cell the search has not reached yet is fogged. Play it.
The fog is the honest picture. You and I can see the whole map and eyeball a route; the algorithm cannot. It has the start cell and nothing else, and every expansion peels back a little more of the world. It only learns there is a wall by asking for a cell's children and not getting one. Any time you find yourself thinking “obviously it should go left, the gap is on the left” — that is you using a cell the algorithm has not discovered yet. It cannot use what it has not seen. Keeping that fog in mind is most of understanding why these searches cost what they cost.
A harder crossing: three walls, one gap each. Depth-first search will happily follow a corridor forever; give it a leash.
Someone tells you the goal is at most 8 steps away, so you run depth-first search and forbid it from going deeper than 8. It comes back saying no solution. The goal is 12 steps away. Is the algorithm wrong?
No, the limit was wrong, and the algorithm cannot tell the difference between “there is no solution” and “there is no solution within 8.” It expanded 75 cells, hit the leash everywhere, and reported failure on a maze that has a perfectly good 12-step answer.
That is what it means for an algorithm to be incomplete: there is a solution and it returns none. Set the limit to 12 and the same code finds it in 15 expansions.
Depth-limited search with the wrong limit, the right limit, and then all of them in turn.
You do not know the right limit. What is the dumbest thing that would work?
If 8 is too small, what would you try next? And after that?
Try 0. Then 1. Then 2. Keep going until one of them finds it. That is iterative deepening, and because the first limit that succeeds is the depth of the shallowest solution, it finds the fewest-step path — the same guarantee as breadth-first search, from a depth-first loop.
It looks absurdly wasteful: every round redoes all the rounds before it. Here it took 595 expansions to reach a solution that limit 12 alone found in 15. Before you dismiss it, do the sum in the next question.
Say every cell has b children and the answer is at depth d. An exhaustive search to depth d looks at about bd nodes. Iterative deepening looks at b0 + b1 + … + bd. How much bigger is that?
Not much. Each term is b times the one before, so the whole sum is at most bd · b/(b−1). For b = 3 that is 1.5 × bd; for b = 10 it is 1.1 ×. The last round dominates everything before it — a side effect of exponential growth working in your favour for once.
Then why did our run cost 30× a single search, not 1.5×? Because bd counts paths, and on a grid there are many paths to the same cell. Depth-first search, having no closed set, walks them all. Remember that: it is about to matter in a big open room.
For any algorithm you are handed: does it work? how long does it take? how much memory does it need? We have been avoiding the third.
What is the main thing that takes up memory in the six-line loop? And at its biggest, how much is on there for breadth-first search versus depth-first search?
What does each one hold when it is deepest into the search?
The fringe. Breadth-first search holds an entire layer at once — every cell at the current distance — and in a space where every state has b new children that is bd nodes. Depth-first search holds the current path plus the untried siblings along it: about b per level, b·m in total, where m is how deep it goes. One is exponential in the depth, the other is linear in it.
That is the answer to the parked question. You keep depth-first search for the memory. In the corn-maze version — put a hand on the wall and walk — you store nothing at all but where you are now, and you can forget everything else and still get out.
| Environment | BFS | DFS |
|---|---|---|
| Nine cells | 3 | 4 |
| A bigger room | 6 | 10 |
| The first wall | 6 | 10 |
| The full maze (later) | 10 | 6 |
Look at the measured peak frontiers above. The theory says exponential versus linear. The numbers say “a bit smaller.” What is different about a grid?
A grid is not a tree. Breadth-first's frontier on a grid is the edge of the explored diamond — it grows like a circle's circumference, roughly 4d cells, not bd. Every cell has four neighbours but most of them are already explored, so the effective b is barely above 1.
The exponential gap is real in spaces that are tree-like: a Rubik's cube, a game tree, a puzzle where every move opens genuinely new states. There breadth-first search runs out of memory long before it runs out of time, and a depth-first loop is the only thing that fits. That is where depth-first search lives, and it is why game engines are built on iterative deepening — breadth-first's answer, on depth-first's memory.
The smallest map that can show the next idea: two cells of water on the direct route, five times the cost of dry ground to wade through. Every algorithm so far assumed every action costs the same.
Breadth-first search finds the 6-step path straight through the water. Is that the best path?
Best with respect to what?
It is the best path by number of steps, which is exactly and only what breadth-first search promised. But it costs 14: four dry cells and two wet ones at 5 each. The way round is 10 steps of dry ground and costs 10.
“Shortest” was never the question. Cheapest was. When actions have different costs, the fewest-step path can be the most expensive one, and breadth-first search will hand it to you with a guarantee attached.
Compare BFS and UCS. Same loop, one more number on each fringe entry.
You want the loop to find the cheapest path, not the shortest. What single change to fringe.remove() does it?
Hand back the entry with the lowest cost so far, instead of the oldest. The fringe becomes a priority queue keyed on path cost. That is uniform-cost search — you may know it as Dijkstra's algorithm — and it is the only algorithm in this lecture that is optimal with respect to cost.
One subtlety: it must test for the goal when it removes it, not when it first generates it, because a cheaper route to the goal may still be sitting in the queue. And on a map where every step costs 1, cheapest-first is oldest-first: uniform-cost search expanded 36 cells on the bigger room where breadth-first expanded 34. The difference only exists when costs do.
MiniGrid's four-rooms environment: a 19×19 world, 260 free cells, doorways between rooms. Big enough for two of our algorithms to behave very differently from the theory.
You know exactly where the goal is. So run two searches: one outward from the start, one outward from the goal, and stop when they meet. Theory says each only needs to go half the depth, so the work drops from bd to 2·bd/2. Breadth-first expanded 82 cells here. Predict the bidirectional count.
It expanded 78. Same 13-step answer, and a saving of about 5% — and on the full maze later it will be 76 against 110, about 31%. Useful. Not the square root the theory promised.
Same reason as the memory question: on a grid the frontier grows like a circle's edge, so two half-size circles cover about half the area of one full one — a constant factor. The square-root saving is real where the frontier grows exponentially. The Kevin Bacon game is the classic case: actors linked by films, six steps between almost any two, and a frontier that multiplies at every step. Search from both ends and the middle is a handful of films instead of half of Hollywood.
Then select DFS. The number in the expanded box is not a typo.
Depth-first search keeps its memory small by not keeping a closed set — it only checks the path it is currently on. Now put it in a big open room. What goes wrong?
It finds a 101-step path to a goal 13 steps away, and it expands 255,430 cells to do it — on a map that has 260. It is visiting the same cells over and over along different paths, because it has no way to know it has been there. In an open room there are astronomically many paths through the same few cells, and tree search walks them.
This is the flip side of the parked question. Forgetting is what makes the memory small, and forgetting is what makes it revisit. The closed set was never optional on a grid; it is what turns bd paths into 260 cells.
A 19×13 maze with a direct route through deep water and a longer way round on dry ground. This is the example everything has been building toward.
Before you press anything: rank the six by the cost of the path they return. Then by how many cells they expand. Then by peak memory. Write it down.
Who promised the fewest steps? Who promised the cheapest? Who promised nothing?
Cost: uniform-cost search alone finds the cheap way round (42). Breadth-first, iterative deepening and bidirectional all find the fewest-step path — and pay 118 for wading through the water, nearly three times the optimum. Depth-first returns 88, better than breadth-first here purely by luck. Depth-limited at 20 returns nothing at all.
Work: depth-first is cheapest (29), then bidirectional (76), uniform-cost (94), breadth-first (110) — and iterative deepening at 2,577, redoing its shallow rounds. Memory: depth-first smallest (6), bidirectional largest (11), holding two frontiers.
Every algorithm in the lecture on the same map. Use the Undiscovered view to see how much of the world each one had to reveal.
| Algorithm | Path cost | Steps | Expanded | Peak frontier |
|---|---|---|---|---|
| BFS | 118 | 26 | 110 | 10 |
| DFS | 88 | 28 | 29 | 6 |
| DLS (limit 20) | none | — | 182 | 7 |
| IDDFS | 118 | 26 | 2,577 | 9 |
| UCS | 42 | 38 | 94 | 9 |
| Bidirectional | 118 | 26 | 76 | 11 |
Nothing here is new. Every row is something you saw happen on a map above; the table just names it. b is the branching factor, d the depth of the shallowest goal, m the maximum depth, ℓ the limit, C* the optimal cost, ε the smallest action cost.
| Algorithm | Complete? | Cost-optimal? | Time | Space |
|---|---|---|---|---|
| BFS | Yes (b finite) | Only if every step costs the same | O(bd) | O(bd) |
| DFS | No (infinite depth, loops) | No | O(bm) | O(bm) |
| Depth-limited | No (fails if d > ℓ) | No | O(bℓ) | O(bℓ) |
| IDDFS | Yes (b finite) | Only if every step costs the same | O(bd) | O(bd) |
| UCS | Yes (costs ≥ ε > 0) | Yes | O(b1+⌊C*/ε⌋) | O(b1+⌊C*/ε⌋) |
| Bidirectional | Yes (BFS both ways) | Only if every step costs the same | O(bd/2) | O(bd/2) |
Three things worth saying out loud, because they are the usual mistakes. “Breadth-first is optimal” — only for steps, and only when every step costs the same, which is exactly when uniform-cost search would have done the same thing. “Iterative deepening wastes most of its work” — a constant factor, because the last round dominates; what wastes work on a grid is tree search, not deepening. “Bidirectional is twice as fast” — it is exponentially better where the frontier grows exponentially, a constant factor where it does not, and useless when you cannot name the goal state or run actions backwards.
These are not preferences. Each algorithm wins where the problem has a particular structure, and the use cases are that structure showing through.
Expand the shallowest node. A queue.
Used for: Web crawling and search-engine indexing, spreading out level by level from a seed page; “degrees of separation” in a social graph; peer discovery in early peer-to-peer networks.
All those graphs are unweighted and you want the fewest hops. That is exactly BFS's guarantee, and a crawler wants breadth before depth so it does not vanish down one site.
Expand the deepest node. A stack.
Used for: Japanese nonograms and Sudoku; maze generation; topological sorting and cycle detection in compilers and build systems.
Constraint puzzles need any solution, not the shallowest, and every solution sits at the same depth. DFS keeps one partial assignment at a time, so memory is linear — and backtracking is exactly what you want when a partial fill becomes impossible.
DFS that refuses to go past depth ℓ.
Used for: Crawling a site with a depth cap so a calendar page cannot generate links forever; fixed-ply lookahead in a game engine.
It fixes DFS's one fatal flaw, an infinite or cyclic space. The price is that you must guess ℓ — guess too low and it returns nothing even though a solution exists.
Depth-limited search at 0, 1, 2, …
Used for: Chess and other game engines, which deepen until the clock runs out and play the best line found so far; any search where the depth is unknown and memory is tight.
BFS's guarantee at DFS's memory cost, and the repeated work is a constant factor, not an extra exponential. In a game engine the repetition is a feature: you always hold a complete answer for the depth you finished.
Expand the cheapest node. A priority queue.
Used for: Road routing where edges are distances, times or tolls; least-latency network routing; cheapest multi-leg flights.
This is Dijkstra's algorithm. The moment your actions have different costs it is the only algorithm here that returns the cheapest solution — which is why the water defeats every other one.
Two frontiers, meeting in the middle.
Used for: Word ladders; Rubik's cube and other puzzles with a known solved state; the shortest connection between two named people in a social graph.
It needs a specific goal state and reversible actions. Where the frontier grows exponentially, two half-depth searches are vastly cheaper than one; on a grid, where it grows like a circle's edge, the saving is only a constant.
You have watched depth-first search lose on every grid in this lecture, so it is worth seeing the one kind of problem it was made for. A nonogram: fill cells so every row and column matches its run clues. Solve it one row at a time and backtrack the moment a column clue becomes impossible.
Watch rows appear and then vanish: that is backtracking. The search commits to a filling of one row, discovers a few rows later that no legal row exists, and unwinds.
Every solution sits at exactly the same depth — one per row — so breadth-first's guarantee buys nothing, and it must hold a whole level of partial grids to get there. There are no costs, so uniform-cost search is BFS. And there is no goal state to search back from, so bidirectional search is undefined. The problem's shape leaves one tool.
One last environment, and the reason there is a next lecture. An open room, agent in one corner, goal in the opposite one. No walls, no water, no trick — just size.
The room is 64×64: 3,844 free cells. The goal is 122 steps away in a dead straight diagonal. How many cells does breadth-first search expand before it finds it?
Where does BFS think the goal might be? Where does it not look?
3,842. Every free cell in the room except two. It has no idea where the goal is, so it looks everywhere, in every direction, in rings. Play it in the Undiscovered view and watch the fog clear across the entire room before the goal turns up in the last corner.
Play BFS to the end. Then select A* and play that. Same room, same start, same goal, same answer.
| Room | Free cells | Path length | BFS expanded | BFS time | A* expanded | A* time | Ratio |
|---|---|---|---|---|---|---|---|
| 8×8 | 36 | 10 | 34 | 0 ms | 11 | 0.0 ms | 3× |
| 16×16 | 196 | 26 | 194 | 0 ms | 27 | 0.1 ms | 7× |
| 32×32 | 900 | 58 | 898 | 2 ms | 59 | 0.2 ms | 15× |
| 64×64 | 3,844 | 122 | 3,842 | 7 ms | 123 | 0.4 ms | 31× |
| 128×128 | 15,876 | 250 | 15,874 | 34 ms | 251 | 0.8 ms | 63× |
| 256×256 | 64,516 | 506 | 64,514 | 128 ms | 507 | 1.6 ms | 127× |
| 512×512 | 260,100 | 1018 | 260,098 | 564 ms | 1,019 | 3.0 ms | 255× |
| 1024×1024 | 1,044,484 | 2042 | 1,044,482 | 2,465 ms | 2,043 | 6.5 ms | 511× |
Breadth-first's work goes up with the area of the room; double the side and it quadruples. At 1024×1024 it examined 1,044,482 cells and took 2.5 seconds to find a goal 2,042 steps away in a straight line. Keep doubling: a 4096×4096 room — a city block at one metre per cell — is 16,760,836 cells, and at this rate about 40 seconds; a 65,536-cell-a-side map is about 3 hours. Every algorithm in this lecture would do roughly the same. What did none of them use?
Where the goal is. All six are uninformed: they know how to generate children and how to recognise a goal when they are standing on it, and nothing else. Given a goal in the opposite corner they search up, left, and back toward the start with the same enthusiasm as down and right, because they cannot tell the difference.
The A* row is the same six-line loop with one extra number in each fringe entry — a guess at how far the goal still is — and it expanded 123 cells to breadth-first's 3,842: it walked in a straight line. On the 1024×1024 room it is 511× less work — 2,043 cells in 6 ms against 1,044,482 in 2.5 s — and the gap keeps widening with the room. That guess is called a heuristic, choosing it well is most of what the next lecture is about, and choosing it badly can make things worse; that is why it gets a whole lecture.
Cells only grow with area. Give the robot a heading and every cell becomes four states. Give it a key to carry and a door to open and they double again. Move off grids entirely — a Rubik's cube has about 43 quintillion states and a branching factor of 18 — and a blind loop that examines every state before the goal does not finish in your lifetime. Uninformed search is what you do when you know nothing about where the goal is. The next lecture is about what changes when you know a little.
Informed search. Where does that number come from, when can you trust it, and what does “trust” even mean for a guess? That is A*. Then: what happens when the map changes while you are walking it — a door closes, a wall appears — and you would rather repair a plan than throw it away. That is D*.
And one debt from the very first page: the real MiniGrid robot has a heading, and turning costs a step. On the nine-cell room, the straight-line distance says 4 and the true cost is 5. The homework opens with exactly that room, and by the end of it you will know why that missing 1 changes which heuristic you are allowed to use.