{"id":"dsa-backtracking-grid-paths","slug":"grid-paths","title":"Paths and words in a grid","text":"A list of strings, one per row, can hold a maze, a word search or the plan of a garden, with `#` for a wall and `.` for an open cell. A search for paths through it starts at one cell and keeps stepping to a neighbor (down, up, right or left), one cell per call: each step is a choice, and the neighbors are the options. The search must remember which cells the current path already covers and never step onto one of them again. Otherwise two open cells side by side would send it back and forth forever.\n\nThat memory is a set of `(row, col)` tuples, `on_path`, and it works differently from the `dist` dict of breadth-first search in the graphs module. Breadth-first search wants one shortest way to each cell, so the first visit settles a cell for good. A search for **every** path can't do that: a cell one path has left behind may be just what another path needs. So a cell goes into `on_path` when the path steps onto it (choose) and comes out again when the search returns from it (un-choose). This program counts every path from the top-left cell to the bottom-right one that never visits a cell twice:\n\n```python\ndef count_paths(grid, row, col, on_path):\n    if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]):\n        return 0                                  # off the grid\n    if grid[row][col] == \"#\" or (row, col) in on_path:\n        return 0                                  # a wall, or already on this path\n    if (row, col) == (len(grid) - 1, len(grid[0]) - 1):\n        return 1                                  # reached the exit\n    on_path.add((row, col))                       # mark: the path is here\n    total = (count_paths(grid, row + 1, col, on_path)\n             + count_paths(grid, row - 1, col, on_path)\n             + count_paths(grid, row, col + 1, on_path)\n             + count_paths(grid, row, col - 1, on_path))\n    on_path.remove((row, col))                    # unmark on the way back\n    return total\n\n\nmaze = [\"....\",\n        \".#..\",\n        \"....\"]\nprint(count_paths(maze, 0, 0, set()))\nprint(count_paths([\".....\", \".....\", \".....\"], 0, 0, set()))\n```\n\n```output\n7\n125\n```\n\n### How it works\n\n- Each call first looks at its own cell: off the grid, a wall or a cell already on the path ends the call with 0. That's why the four recursive calls need no checks in front of them: a step to a bad neighbor simply comes back with 0.\n- `on_path` is a set, so adding a cell, asking whether a cell is in it and removing it are all O(1).\n- The small maze has 7 paths. Take its wall away and add a fifth column, and there are 125, most of them long and winding: the search follows every one of them to the end.\n\n### Forgetting to unmark\n\nLeave out the unmark, and the set works like the visited check of breadth-first search:\n\n```python\ndef count_paths(grid, row, col, seen):\n    if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]):\n        return 0\n    if grid[row][col] == \"#\" or (row, col) in seen:\n        return 0\n    if (row, col) == (len(grid) - 1, len(grid[0]) - 1):\n        return 1\n    seen.add((row, col))                          # marked for good\n    return (count_paths(grid, row + 1, col, seen)\n            + count_paths(grid, row - 1, col, seen)\n            + count_paths(grid, row, col + 1, seen)\n            + count_paths(grid, row, col - 1, seen))\n\n\nmaze = [\"....\",\n        \".#..\",\n        \"....\"]\nprint(count_paths(maze, 0, 0, set()))\n```\n\n```output\n2\n```\n\nThe maze has 7 paths, not 2. The first paths that reach the exit leave their cells marked, and every later path that would cross them is blocked, even though it's a different path. There's no error, just a wrong number, so look for the unmark whenever a grid search finds too few paths.\n\n### What it costs\n\nThe number of paths grows exponentially with the grid: 2 for a 2 × 2 grid, 12 for 3 × 3, 184 for 4 × 4, 8,512 for 5 × 5 and over a million for 6 × 6, and the search visits every one of them. A path can be as long as the grid has cells, and so can the recursion: fine for these small grids, but a path on a 40 × 40 grid could go 1,600 calls deep, past Python's limit of about 1000.\n\n### Word search\n\nWord search games hide words in a grid of letters. Each letter of a word sits on a cell next to the one before it (up, down, left or right), and no cell may be used twice in one word. The same kind of search finds them. Any cell could hold the first letter, so start a search from each one in turn. From a cell, matching the word from its letter `k` on goes like this:\n\n1. If `k == len(word)`, every letter has matched: `True`.\n2. If the cell is off the grid, already on the path, or its letter isn't `word[k]`: `False`.\n3. Otherwise mark the cell, try the four neighbors with `k + 1`, unmark the cell, and return whether any neighbor worked.\n\nTwo kinds of pruning keep it quick. A cell with the wrong letter ends its branch at once, so the search only follows paths that spell the start of the word. And `or` stops at the first neighbor that works, so the search ends as soon as one path spells the whole word.\n","task":"Some puzzles want a path that walks over **every** open cell exactly once. Fill in the blanks: a path counts only when it reaches the exit having covered every open cell, and the cell must be unmarked on the way back. The program should print:\n\n```\n2\n8\n1\n0\n```\n","kind":"fill","seed":"def tours(grid, row, col, on_path, open_cells):\n    if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]):\n        return 0\n    if grid[row][col] == \"#\" or (row, col) in on_path:\n        return 0\n    on_path.add((row, col))\n    if (row, col) == (len(grid) - 1, len(grid[0]) - 1):\n        total = 1 if [[len(on_path) == open_cells‖open_cells == len(on_path)]] else 0\n    else:\n        total = (tours(grid, row + 1, col, on_path, open_cells)\n                 + tours(grid, row - 1, col, on_path, open_cells)\n                 + tours(grid, row, col + 1, on_path, open_cells)\n                 + tours(grid, row, col - 1, on_path, open_cells))\n    [[on_path.remove((row, col))‖on_path.discard((row, col))]]\n    return total\n\n\ndef count_tours(grid):\n    open_cells = sum(line.count(\".\") for line in grid)\n    return tours(grid, 0, 0, set(), open_cells)\n\n\nprint(count_tours([\"...\", \"...\", \"...\"]))\nprint(count_tours([\".....\", \".....\", \".....\"]))\nprint(count_tours([\".#..\", \"....\", \"..#.\"]))\nprint(count_tours([\"....\", \".#..\", \"....\"]))\n","solution":"def tours(grid, row, col, on_path, open_cells):\n    if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]):\n        return 0\n    if grid[row][col] == \"#\" or (row, col) in on_path:\n        return 0\n    on_path.add((row, col))\n    if (row, col) == (len(grid) - 1, len(grid[0]) - 1):\n        total = 1 if len(on_path) == open_cells else 0\n    else:\n        total = (tours(grid, row + 1, col, on_path, open_cells)\n                 + tours(grid, row - 1, col, on_path, open_cells)\n                 + tours(grid, row, col + 1, on_path, open_cells)\n                 + tours(grid, row, col - 1, on_path, open_cells))\n    on_path.remove((row, col))\n    return total\n\n\ndef count_tours(grid):\n    open_cells = sum(line.count(\".\") for line in grid)\n    return tours(grid, 0, 0, set(), open_cells)\n\n\nprint(count_tours([\"...\", \"...\", \"...\"]))\nprint(count_tours([\".....\", \".....\", \".....\"]))\nprint(count_tours([\".#..\", \"....\", \"..#.\"]))\nprint(count_tours([\"....\", \".#..\", \"....\"]))\n","hints":["At the exit, the exit cell is already in `on_path`. The path covered every open cell when the set holds as many cells as there are open cells.","The first blank compares `len(on_path)` with `open_cells`. The second blank undoes `on_path.add((row, col))`."],"tests":[{"name":"Output","stdin":"","expect":"2\n8\n1\n0","hidden":false}],"require":[],"forbid":[],"more":[{"task":"Write `exists(grid, word)`: `True` when the grid hides `word` by the rules of the lesson (each letter on a cell up, down, left or right of the one before, and no cell used twice in the word), and `False` otherwise. `grid` is a list of strings, one per row.\n\nFor example, this program:\n\n```python\ngrid = [\"CATS\",\n        \"OHEN\",\n        \"DOGE\"]\nprint(exists(grid, \"CATS\"), exists(grid, \"COD\"))\nprint(exists(grid, \"NET\"), exists(grid, \"TACT\"))\n```\n\nprints:\n\n```\nTrue True\nTrue False\n```\n","kind":"code","seed":"def exists(grid, word):\n    pass\n\n\nif __name__ == \"__main__\":\n    grid = [\"CATS\",\n            \"OHEN\",\n            \"DOGE\"]\n    print(exists(grid, \"CATS\"), exists(grid, \"COD\"))\n    print(exists(grid, \"NET\"), exists(grid, \"TACT\"))\n","solution":"def found_from(grid, word, k, row, col, on_path):\n    if k == len(word):\n        return True\n    if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]):\n        return False\n    if (row, col) in on_path or grid[row][col] != word[k]:\n        return False\n    on_path.add((row, col))\n    found = (found_from(grid, word, k + 1, row + 1, col, on_path)\n             or found_from(grid, word, k + 1, row - 1, col, on_path)\n             or found_from(grid, word, k + 1, row, col + 1, on_path)\n             or found_from(grid, word, k + 1, row, col - 1, on_path))\n    on_path.remove((row, col))\n    return found\n\n\ndef exists(grid, word):\n    for row in range(len(grid)):\n        for col in range(len(grid[0])):\n            if found_from(grid, word, 0, row, col, set()):\n                return True\n    return False\n\n\nif __name__ == \"__main__\":\n    grid = [\"CATS\",\n            \"OHEN\",\n            \"DOGE\"]\n    print(exists(grid, \"CATS\"), exists(grid, \"COD\"))\n    print(exists(grid, \"NET\"), exists(grid, \"TACT\"))\n","hints":["Write a helper `found_from(grid, word, k, row, col, on_path)`: can the letters from `word[k]` on be traced starting at `(row, col)`? Follow the three steps of the lesson.","In the helper, return `False` for a cell off the grid, a cell in `on_path` or a cell whose letter isn't `word[k]`. Then add the cell to `on_path`, try the four neighbors with `k + 1`, joined with `or`, remove the cell, and return the result.","`exists` calls the helper from every cell, with `k = 0` and an empty set, and returns `True` as soon as one start works."],"tests":[{"name":"Test 1","stdin":"","call":"grid = [\"CATS\",\n        \"OHEN\",\n        \"DOGE\"]\nprint(exists(grid, \"CATS\"), exists(grid, \"COD\"))\nprint(exists(grid, \"NET\"), exists(grid, \"TACT\"))\n","expect":"True True\nTrue False","hidden":false},{"name":"Test 2","stdin":"","call":"grid = [\"CATS\",\n        \"OHEN\",\n        \"DOGE\"]\nprint(exists(grid, \"HOG\"), exists(grid, \"GOOD\"), exists(grid, \"TEN\"))\nprint(exists(grid, \"GENE\"), exists(grid, \"GEE\"), exists(grid, \"EGE\"))\nprint(exists(grid, \"GEG\"), exists(grid, \"HOH\"), exists(grid, \"CHAT\"))\n","expect":"True False True\nTrue False True\nFalse False False","hidden":true},{"name":"print(exists([\"DOG\"], \"D\"), exists([\"DOG\"], \"Z\"))","stdin":"","call":"print(exists([\"DOG\"], \"D\"), exists([\"DOG\"], \"Z\"))\n","expect":"True False","hidden":true},{"name":"print(exists([\"AB\"], \"ABA\"), exists([\"AB\"], \"BA\"))","stdin":"","call":"print(exists([\"AB\"], \"ABA\"), exists([\"AB\"], \"BA\"))\n","expect":"False True","hidden":true},{"name":"print(exists([\"AB\", \"CD\"], \"ABDC\"), exists([\"AB\", \"CD\"], \"ABCD\"))","stdin":"","call":"print(exists([\"AB\", \"CD\"], \"ABDC\"), exists([\"AB\", \"CD\"], \"ABCD\"))\n","expect":"True False","hidden":true},{"name":"exists([\"AAA\", \"AAA\", \"AAA\"], \"AAAAAAAAAB\")","stdin":"","call":"exists([\"AAA\", \"AAA\", \"AAA\"], \"AAAAAAAAAB\")\n","expect":"False","hidden":true,"returns":true},{"name":"print(exists([\"AA\", \"BA\", \"BB\"], \"AABAB\"), exists([\"ABB\", \"ABB\"], \"ABBBBA\"))","stdin":"","call":"print(exists([\"AA\", \"BA\", \"BB\"], \"AABAB\"), exists([\"ABB\", \"ABB\"], \"ABBBBA\"))\n","expect":"True True","hidden":true}],"require":[],"forbid":[]},{"task":"Breadth-first search finds the **shortest** way through a maze. The **longest** way that never visits a cell twice is a harder question: no quick method is known, and backtracking has to try every path. Write `longest_path(grid)`: the number of cells on the longest path from the top-left cell to the bottom-right one, moving up, down, left or right through `.` cells and never visiting a cell twice. Return 0 when there is no path at all.\n\nFor example, `print(longest_path([\"....\", \".#..\", \"....\"]), longest_path([\"..\", \".#\", \"..\"]))` prints this (the longest way through the first maze misses just one of its 11 open cells, and the second has only one way, down the left side):\n\n```\n10 4\n```\n","kind":"code","seed":"def longest_path(grid):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(longest_path([\"....\", \".#..\", \"....\"]), longest_path([\"..\", \".#\", \"..\"]))\n","solution":"def longest_from(grid, row, col, on_path):\n    if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]):\n        return 0\n    if grid[row][col] == \"#\" or (row, col) in on_path:\n        return 0\n    if (row, col) == (len(grid) - 1, len(grid[0]) - 1):\n        return len(on_path) + 1\n    on_path.add((row, col))\n    best = max(longest_from(grid, row + 1, col, on_path),\n               longest_from(grid, row - 1, col, on_path),\n               longest_from(grid, row, col + 1, on_path),\n               longest_from(grid, row, col - 1, on_path))\n    on_path.remove((row, col))\n    return best\n\n\ndef longest_path(grid):\n    return longest_from(grid, 0, 0, set())\n\n\nif __name__ == \"__main__\":\n    print(longest_path([\"....\", \".#..\", \"....\"]), longest_path([\"..\", \".#\", \"..\"]))\n","hints":["Start from the lesson's `count_paths`. Instead of adding the counts of the four neighbors, take the `max` of what they return.","At the exit, the path's length is the number of cells on it so far plus the exit itself: `len(on_path) + 1`. A dead end returns 0, so it never wins the `max` against a real path.","Mark the cell before the four calls and unmark it after them. Without the unmark, the cells of the first paths stay blocked and the longest way may never be tried."],"tests":[{"name":"print(longest_path([\"....\", \".#..\", \"....\"]), longest_path([\"..\", \".#\", \"..\"]))","stdin":"","call":"print(longest_path([\"....\", \".#..\", \"....\"]), longest_path([\"..\", \".#\", \"..\"]))\n","expect":"10 4","hidden":false},{"name":"longest_path([\"..\", \"..\"])","stdin":"","call":"longest_path([\"..\", \"..\"])\n","expect":"3","hidden":true,"returns":true},{"name":"longest_path([\".\"])","stdin":"","call":"longest_path([\".\"])\n","expect":"1","hidden":true,"returns":true},{"name":"print(longest_path([\"#.\"]), longest_path([\"..#\", \"##.\"]))","stdin":"","call":"print(longest_path([\"#.\"]), longest_path([\"..#\", \"##.\"]))\n","expect":"0 0","hidden":true},{"name":"longest_path([\"...\", \"...\", \"...\"])","stdin":"","call":"longest_path([\"...\", \"...\", \"...\"])\n","expect":"9","hidden":true,"returns":true},{"name":"longest_path([\"....\", \"....\", \"....\"])","stdin":"","call":"longest_path([\"....\", \"....\", \"....\"])\n","expect":"12","hidden":true,"returns":true},{"name":"longest_path([\"....\", \".##.\", \"....\"])","stdin":"","call":"longest_path([\"....\", \".##.\", \"....\"])\n","expect":"6","hidden":true,"returns":true},{"name":"longest_path([\"....\", \".#..\", \"...#\", \"#...\"])","stdin":"","call":"longest_path([\"....\", \".#..\", \"...#\", \"#...\"])\n","expect":"11","hidden":true,"returns":true}],"require":[],"forbid":[]}]}