{"id":"dsa-graphs-bfs","slug":"bfs","title":"Breadth-first search","text":"A rumor starts with one person. In the first hour, everyone that person talks to hears it. In the second hour, everyone *they* talk to hears it, and so on. The hour in which you first hear it is the number of links in the shortest chain of people from the first person to you. **Breadth-first search** (BFS) works out that number for every vertex of a graph in which each edge counts as one step: it finds the vertices one edge from the start, then the ones two edges away, then three.\n\nIt's the level-order walk of the module on trees, with one change. A tree has only one way down to each node, so level order never meets a node twice. A graph can lead back to a vertex along another edge or around a cycle, so BFS must remember which vertices it has found and never queue one again. In Python, that memory is a dict, `dist`, from each vertex found so far to its number of steps: `w not in dist` asks \"is w new?\" in O(1) on average, and `dist[w]` is w's distance.\n\nThe search puts the start vertex, at distance 0, in a queue, a `collections.deque`. Then, until the queue is empty, it takes the vertex at the front, and each neighbor of it that is new gets that vertex's distance plus 1 and joins the back of the queue.\n\n```python\nfrom collections import deque\n\n\ndef bfs(graph, start):\n    dist = {start: 0}\n    queue = deque([start])\n    while queue:\n        v = queue.popleft()\n        for w in graph[v]:\n            if w not in dist:\n                dist[w] = dist[v] + 1\n                queue.append(w)\n    return dist\n\n\ngraph = {\n    \"A\": [\"B\", \"C\"],\n    \"B\": [\"A\", \"D\"],\n    \"C\": [\"A\", \"D\", \"E\"],\n    \"D\": [\"B\", \"C\", \"F\"],\n    \"E\": [\"C\", \"F\"],\n    \"F\": [\"D\", \"E\"],\n    \"G\": [],\n}\nprint(bfs(graph, \"A\"))\nprint(bfs(graph, \"G\"))\n```\n\n```output\n{'A': 0, 'B': 1, 'C': 1, 'D': 2, 'E': 2, 'F': 3}\n{'G': 0}\n```\n\nG has no edges, so the search from A never gets there: G is **unreachable** from A, and has no distance. The search from G finds only G itself.\n\n### Why the first visit is the shortest\n\nThe queue holds the vertices in rings around the start: the ones at distance 1, then the ones at distance 2, and so on. All the vertices at distance d leave the queue before any at distance d + 1, and the neighbors they add get d + 1, at the back. So vertices leave in order of distance. When BFS first reaches w from v, every vertex nearer the start than v has already left the queue, and none of them had an edge to w, or w would have been found sooner. So there is no shorter way to w, and its distance never needs to change. D is found from B, at 2. C finds it again, also at 2, but D is already in `dist` and is skipped.\n\n### Why it needs the visited check\n\nWithout the `if w not in dist` check, a vertex joins the queue again every time it's reached. In a triangle, each vertex sends the other two back into the queue, forever:\n\n```python\nfrom collections import deque\n\ngraph = {\"A\": [\"B\", \"C\"], \"B\": [\"A\", \"C\"], \"C\": [\"A\", \"B\"]}\nqueue = deque([\"A\"])\ntaken = []\nfor _ in range(10):\n    v = queue.popleft()\n    taken.append(v)\n    for w in graph[v]:\n        queue.append(w)\nprint(taken)\nprint(len(queue), \"waiting\")\n```\n\n```output\n['A', 'B', 'C', 'A', 'C', 'A', 'B', 'B', 'C', 'A']\n11 waiting\n```\n\nThis loop stops only because it was told to stop after 10 rounds. As `while queue:`, it would never end: each round takes one vertex out and puts two in. Mark a vertex when it *joins* the queue, as `dist[w] = ...` does, rather than when it leaves: then each vertex joins once, however many edges lead to it.\n\n### On a grid\n\nA maze on a grid is a graph without an adjacency list. Each open cell `(r, c)` is a vertex, and its neighbors are the open cells above, below, left and right of it, worked out when needed. Cells outside the grid and walls (`#`) are skipped:\n\n```python\nfrom collections import deque\n\n\ndef steps(grid, start, goal):\n    rows, cols = len(grid), len(grid[0])\n    dist = {start: 0}\n    queue = deque([start])\n    while queue:\n        r, c = queue.popleft()\n        if (r, c) == goal:\n            return dist[(r, c)]\n        for nr, nc in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:\n            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != \"#\" and (nr, nc) not in dist:\n                dist[(nr, nc)] = dist[(r, c)] + 1\n                queue.append((nr, nc))\n    return -1\n\n\nmaze = [\n    \"...#....\",\n    \".#.#.##.\",\n    \".#...#..\",\n    \".####.##\",\n    \"......#.\",\n]\nprint(steps(maze, (0, 0), (0, 7)))\nprint(steps(maze, (0, 0), (4, 5)))\nprint(steps(maze, (0, 0), (4, 7)))\n```\n\n```output\n11\n9\n-1\n```\n\nThe search stops as soon as the goal leaves the queue. The bottom right corner is walled in, so the queue runs empty first, and -1 says the goal can't be reached.\n\n### What it costs\n\nEach vertex joins the queue at most once and leaves it once, O(1) each with a deque, and each edge is looked at once (from both of its ends, if it's undirected): O(V + E) time, and O(V) memory for `dist` and the queue. A grid of R rows and C columns has R·C cells with at most 4 neighbors each: O(R·C).\n\nTwo Python mistakes keep the answers right and ruin the cost:\n\n- **A list of visited vertices.** `(r, c) not in visited` on a list compares with every cell found so far: O(V) for each check, O(V²) in all. A maze with 45,000 open cells then takes billions of comparisons. Use a set or a dict.\n- **A list as the queue.** `queue.pop(0)` moves every item behind the first one: O(n) for each pop. `deque.popleft()` is O(1).\n\n### The path, not just its length\n\nTo get the path itself, remember where each vertex was found from, in a dict `came_from`: `came_from[w] = v` when w joins the queue, and `came_from[start] = None`. At the goal, follow `came_from` back, one vertex at a time, until you reach the start, collecting the vertices on the way. Then reverse the list. Each vertex was found from a vertex one step nearer the start, so the path is a shortest one.\n","task":"This program runs a BFS on a directed graph and prints each vertex as it leaves the queue, with its distance. What does it print? Type each line it prints.\n","kind":"predict","seed":"from collections import deque\n\ngraph = {1: [2, 5], 2: [3], 3: [1, 4], 4: [], 5: [4, 6], 6: [3]}\ndist = {1: 0}\nqueue = deque([1])\nwhile queue:\n    v = queue.popleft()\n    print(v, dist[v])\n    for w in graph[v]:\n        if w not in dist:\n            dist[w] = dist[v] + 1\n            queue.append(w)\n","solution":"from collections import deque\n\ngraph = {1: [2, 5], 2: [3], 3: [1, 4], 4: [], 5: [4, 6], 6: [3]}\ndist = {1: 0}\nqueue = deque([1])\nwhile queue:\n    v = queue.popleft()\n    print(v, dist[v])\n    for w in graph[v]:\n        if w not in dist:\n            dist[w] = dist[v] + 1\n            queue.append(w)\n","hints":["The queue starts with 1. When 1 leaves, its neighbors 2 and 5 join, in that order, both at distance 1.","Next 2 leaves and 3 joins at distance 2. Then 5 leaves, and 4 and 6 join, also at distance 2.","Edges go one way here. 3 to 1 and 6 to 3 lead to vertices that are already in `dist`, so they change nothing."],"tests":[{"name":"Output","stdin":"","expect":"1 0\n2 1\n5 1\n3 2\n4 2\n6 2","hidden":false}],"require":[],"forbid":[],"lines":["1 0","2 1","5 1","3 2","4 2","6 2"],"more":[{"task":"A maze is a list of strings: `.` is open, `#` is a wall, `S` is the start and `E` the exit. Write `shortest_path(grid)` that returns the fewest moves from S to E, moving up, down, left or right, or -1 when E can't be reached. Keep the cells you've found in a set or a dict: the hidden tests have mazes of 240 by 280 and 301 by 301 cells, with tens of thousands of open cells, where a list of visited cells takes billions of comparisons.\n\nFor example, this program:\n\n```python\nmaze = [\n    \"S..#....\",\n    \".#.#.##.\",\n    \".#...#E.\",\n    \".####.#.\",\n    \"......#.\",\n]\nprint(shortest_path(maze))\n```\n\nprints:\n\n```\n14\n```\n","kind":"code","seed":"from collections import deque\n\n\ndef shortest_path(grid):\n    pass\n\n\nif __name__ == \"__main__\":\n    maze = [\n        \"S..#....\",\n        \".#.#.##.\",\n        \".#...#E.\",\n        \".####.#.\",\n        \"......#.\",\n    ]\n    print(shortest_path(maze))\n","solution":"from collections import deque\n\n\ndef shortest_path(grid):\n    rows, cols = len(grid), len(grid[0])\n    for r in range(rows):\n        for c in range(cols):\n            if grid[r][c] == \"S\":\n                start = (r, c)\n    dist = {start: 0}\n    queue = deque([start])\n    while queue:\n        r, c = queue.popleft()\n        if grid[r][c] == \"E\":\n            return dist[(r, c)]\n        for nr, nc in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:\n            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != \"#\" and (nr, nc) not in dist:\n                dist[(nr, nc)] = dist[(r, c)] + 1\n                queue.append((nr, nc))\n    return -1\n\n\nif __name__ == \"__main__\":\n    maze = [\n        \"S..#....\",\n        \".#.#.##.\",\n        \".#...#E.\",\n        \".####.#.\",\n        \"......#.\",\n    ]\n    print(shortest_path(maze))\n","hints":["First find S: loop over the rows and columns until `grid[r][c] == \"S\"`.","Then run the lesson's grid BFS from S, with a dict `dist` of the cells found so far. Return `dist[(r, c)]` when the cell that leaves the queue is E.","A neighbor joins the queue when it's inside the grid, isn't a wall and isn't in `dist` yet. If the queue runs empty, E can't be reached: return -1."],"tests":[{"name":"Test 1","stdin":"","call":"maze = [\n    \"S..#....\",\n    \".#.#.##.\",\n    \".#...#E.\",\n    \".####.#.\",\n    \"......#.\",\n]\nprint(shortest_path(maze))\n","expect":"14","hidden":false},{"name":"shortest_path([\"E\", \"S\"])","stdin":"","call":"shortest_path([\"E\", \"S\"])\n","expect":"1","hidden":true,"returns":true},{"name":"shortest_path([\".S.\", \"###\", \"..E\"])","stdin":"","call":"shortest_path([\".S.\", \"###\", \"..E\"])\n","expect":"-1","hidden":true,"returns":true},{"name":"shortest_path([\"S.\", \"#.\", \"E.\"])","stdin":"","call":"shortest_path([\"S.\", \"#.\", \"E.\"])\n","expect":"4","hidden":true,"returns":true},{"name":"shortest_path([\".....\", \".S...\", \".....\", \"....E\"])","stdin":"","call":"shortest_path([\".....\", \".S...\", \".....\", \"....E\"])\n","expect":"5","hidden":true,"returns":true},{"name":"shortest_path([\"S..#.\", \".#.#.\", \"..##E\"])","stdin":"","call":"shortest_path([\"S..#.\", \".#.#.\", \"..##E\"])\n","expect":"-1","hidden":true,"returns":true},{"name":"shortest_path([\"E....\", \"####.\", \"S....\", \".###.\", \".....\"])","stdin":"","call":"shortest_path([\"E....\", \"####.\", \"S....\", \".###.\", \".....\"])\n","expect":"10","hidden":true,"returns":true},{"name":"shortest_path([\"S\" + \".\" * 279] + [\".\" * 280] * 238 + [\".\" * 279 + \"E\"])","stdin":"","call":"shortest_path([\"S\" + \".\" * 279] + [\".\" * 280] * 238 + [\".\" * 279 + \"E\"])\n","expect":"518","hidden":true,"big":true,"returns":true},{"name":"Test 9","stdin":"","call":"size = 301\nmaze = [\"\".join(\"#\" if (r % 4 == 1 and c < size - 1) or (r % 4 == 3 and c > 0) else \".\" for c in range(size)) for r in range(size)]\nmaze[0] = \"S\" + maze[0][1:]\nmaze[-1] = maze[-1][:-1] + \"E\"\nprint(shortest_path(maze))\n","expect":"45600","hidden":true,"big":true}],"require":[],"forbid":[]},{"task":"A subway map is a dict: each station maps to a list of the stations one stop away. Write `route(graph, start, goal)` that returns the list of stations on a shortest route, from `start` to `goal`, both included, or `None` when there's no route. In the tests, there is only one shortest route each time.\n\nOne hidden test is a line of 3,000 stations, so walk back from the goal with a loop: a function that calls itself once for each station would go 3,000 calls deep, and Python stops at about 1000.\n\nFor example, this program:\n\n```python\nsubway = {\n    \"Airport\": [\"Docks\"],\n    \"Docks\": [\"Airport\", \"Market\", \"Museum\"],\n    \"Market\": [\"Docks\", \"Stadium\"],\n    \"Museum\": [\"Docks\", \"Park\"],\n    \"Park\": [\"Museum\", \"Library\"],\n    \"Library\": [\"Park\", \"Zoo\"],\n    \"Stadium\": [\"Market\", \"Zoo\"],\n    \"Zoo\": [\"Library\", \"Stadium\"],\n}\nprint(route(subway, \"Airport\", \"Zoo\"))\n```\n\nprints:\n\n```\n['Airport', 'Docks', 'Market', 'Stadium', 'Zoo']\n```\n","kind":"code","seed":"from collections import deque\n\n\ndef route(graph, start, goal):\n    pass\n\n\nif __name__ == \"__main__\":\n    subway = {\n        \"Airport\": [\"Docks\"],\n        \"Docks\": [\"Airport\", \"Market\", \"Museum\"],\n        \"Market\": [\"Docks\", \"Stadium\"],\n        \"Museum\": [\"Docks\", \"Park\"],\n        \"Park\": [\"Museum\", \"Library\"],\n        \"Library\": [\"Park\", \"Zoo\"],\n        \"Stadium\": [\"Market\", \"Zoo\"],\n        \"Zoo\": [\"Library\", \"Stadium\"],\n    }\n    print(route(subway, \"Airport\", \"Zoo\"))\n","solution":"from collections import deque\n\n\ndef route(graph, start, goal):\n    came_from = {start: None}\n    queue = deque([start])\n    while queue:\n        station = queue.popleft()\n        if station == goal:\n            path = []\n            while station is not None:\n                path.append(station)\n                station = came_from[station]\n            path.reverse()\n            return path\n        for nxt in graph[station]:\n            if nxt not in came_from:\n                came_from[nxt] = station\n                queue.append(nxt)\n    return None\n\n\nif __name__ == \"__main__\":\n    subway = {\n        \"Airport\": [\"Docks\"],\n        \"Docks\": [\"Airport\", \"Market\", \"Museum\"],\n        \"Market\": [\"Docks\", \"Stadium\"],\n        \"Museum\": [\"Docks\", \"Park\"],\n        \"Park\": [\"Museum\", \"Library\"],\n        \"Library\": [\"Park\", \"Zoo\"],\n        \"Stadium\": [\"Market\", \"Zoo\"],\n        \"Zoo\": [\"Library\", \"Stadium\"],\n    }\n    print(route(subway, \"Airport\", \"Zoo\"))\n","hints":["Run a BFS from `start` with a dict `came_from` in place of `dist`: `came_from[start] = None`, and `came_from[nxt] = station` when `nxt` joins the queue. Its keys are the stations seen so far.","When the station that leaves the queue is the goal, walk back: append the station, move to `came_from[station]`, and stop after the start, whose entry is `None`.","The walk back collects the stations from the goal to the start, so reverse the list before returning it. If the queue runs empty, return `None`."],"tests":[{"name":"Test 1","stdin":"","call":"subway = {\n    \"Airport\": [\"Docks\"],\n    \"Docks\": [\"Airport\", \"Market\", \"Museum\"],\n    \"Market\": [\"Docks\", \"Stadium\"],\n    \"Museum\": [\"Docks\", \"Park\"],\n    \"Park\": [\"Museum\", \"Library\"],\n    \"Library\": [\"Park\", \"Zoo\"],\n    \"Stadium\": [\"Market\", \"Zoo\"],\n    \"Zoo\": [\"Library\", \"Stadium\"],\n}\nprint(route(subway, \"Airport\", \"Zoo\"))\n","expect":"['Airport', 'Docks', 'Market', 'Stadium', 'Zoo']","hidden":false},{"name":"Test 2","stdin":"","call":"subway = {\n    \"Airport\": [\"Docks\"],\n    \"Docks\": [\"Airport\", \"Market\", \"Museum\"],\n    \"Market\": [\"Docks\", \"Stadium\"],\n    \"Museum\": [\"Docks\", \"Park\"],\n    \"Park\": [\"Museum\", \"Library\"],\n    \"Library\": [\"Park\", \"Zoo\"],\n    \"Stadium\": [\"Market\", \"Zoo\"],\n    \"Zoo\": [\"Library\", \"Stadium\"],\n}\nprint(route(subway, \"Park\", \"Airport\"))\nprint(route(subway, \"Library\", \"Stadium\"))\nprint(route(subway, \"Zoo\", \"Zoo\"))\n","expect":"['Park', 'Museum', 'Docks', 'Airport']\n['Library', 'Zoo', 'Stadium']\n['Zoo']","hidden":true},{"name":"print(route({\"A\": [\"B\"], \"B\": [\"A\"], \"C\": []}, \"A\", \"C\"))","stdin":"","call":"print(route({\"A\": [\"B\"], \"B\": [\"A\"], \"C\": []}, \"A\", \"C\"))\n","expect":"None","hidden":true},{"name":"route({1: [2, 3], 2: [4], 3: [], 4: [3]}, 1, 4)","stdin":"","call":"route({1: [2, 3], 2: [4], 3: [], 4: [3]}, 1, 4)\n","expect":"[1, 2, 4]","hidden":true,"returns":true},{"name":"Test 5","stdin":"","call":"line = {i: [j for j in (i - 1, i + 1) if 0 <= j < 3000] for i in range(3000)}\npath = route(line, 0, 2999)\nprint(len(path), path[:3], path[-1])\n","expect":"3000 [0, 1, 2] 2999","hidden":true}],"require":[],"forbid":[{"pattern":"setrecursionlimit","message":"Don't raise the recursion limit: the browser's Python can crash. Walk back with a loop instead."}]}]}