Exit slides Esc

Unit 13 — Grids, Graphs & Traversal

Slide 1 of 39

  1. What is the fewest number of steps through a maze? How many separate rooms are connected? Both questions get easier when we treat each open location as a NODE and each allowed move as an EDGE. This unit represents those connections and traverses them without getting stuck in cycles — using parallel arrays, a deque, recursion, and a visited set. Each idea is a short executable demo with a Notice, then a full stdin solver.

    • Visit the four neighbours of a grid cell with correct bounds checks.

    • Store a graph as an adjacency-list dictionary and read a node's degree.

    • Flood-fill a region and find the fewest steps with breadth-first search (BFS).

    • Test whether one node can reach another with depth-first search (DFS).

  2. Lesson 1 — Four Neighbours in a Grid; an Adjacency List

    Store a grid as a list of strings. A cell (r, c) has up to FOUR neighbours; add the dr/dc offsets and guard the bounds with 0 <= nr and nr < rows.

  3. grid = ["....", ".##.", "...."]
    rows = 3
    cols = 4
    r = 1
    c = 0
    dr = [-1, 1, 0, 0]
    dc = [0, 0, -1, 1]
    count = 0
    direction = 0
    while direction < 4:
        nr = r + dr[direction]
        nc = c + dc[direction]
        if 0 <= nr and nr < rows and 0 <= nc and nc < cols:
            if grid[nr][nc] == ".":
                count = count + 1
        direction = direction + 1
    print("open neighbours:", count)

    Output

    open neighbours: 2
    
  4. The four offsets (-1,0),(1,0),(0,-1),(0,1) reach up/down/left/right; only in-bounds open (.) cells count.

  5. A general graph is an ADJACENCY LIST — a plain dict mapping each node to its neighbour list. Undirected edges add BOTH directions.

  6. edges = [(1, 2), (2, 3), (2, 4)]
    adj = {}
    for pair in edges:
        u = pair[0]
        v = pair[1]
        if u not in adj:
            adj[u] = []
        if v not in adj:
            adj[v] = []
        adj[u].append(v)
        adj[v].append(u)
    print(adj)

    Output

    {1: [2], 2: [1, 3, 4], 3: [2], 4: [2]}
    

    if u not in adj: adj[u] = [] creates the list on first sight; each edge appends to both endpoints (no defaultdict).

  7. Read a node's neighbours and its degree straight from the dict.

  8. adj = {}
    adj[2] = [1, 3, 4]
    node = 2
    print("neighbours:", adj[node])
    print("degree:", len(adj[node]))

    Output

    neighbours: [1, 3, 4]
    degree: 3
    

    adj[node] is the neighbour list; len(adj[node]) is the degree.

  9. Put it together: the program reads E edges then a query node, builds the adjacency list, and prints the node's degree.

  10. Try it
    import sys
    
    data = sys.stdin.read()
    tokens = data.split()
    edges = int(tokens[0])
    adj = {}
    position = 1
    i = 0
    while i < edges:
        u = int(tokens[position])
        v = int(tokens[position + 1])
        if u not in adj:
            adj[u] = []
        if v not in adj:
            adj[v] = []
        adj[u].append(v)
        adj[v].append(u)
        position = position + 2
        i = i + 1
    node = int(tokens[position])
    print(str(len(adj[node])))
  11. Run the full solver from this unit folder:

    python assets/l1.py < assets/l1/1.in
  12. Build the undirected adjacency dict, then print len(adj[node]).

    Complexity: O(E) — one pass over the edges.

  13. Lesson 2 — Recursive Flood-Fill & Breadth-First Search

    FLOOD-FILL a region: recurse to the 4 neighbours, using a visited set (passed down) and base-casing bounds / walls (#) / already-visited.

  14. grid = ["..#.", ".##.", "...."]
    rows = 3
    cols = 4
    visited = set()
    
    def fill(r, c, visited):
        if not (0 <= r and r < rows and 0 <= c and c < cols):
            return
        if grid[r][c] == "#" or (r, c) in visited:
            return
        visited.add((r, c))
        fill(r - 1, c, visited)
        fill(r + 1, c, visited)
        fill(r, c - 1, visited)
        fill(r, c + 1, visited)
    
    fill(0, 0, visited)
    print("region size:", len(visited))

    Output

    region size: 9
    
  15. fill marks the cell then recurses in all four directions; the visited set (an argument) stops cycles and counts the region.

  16. A deque is a FIFO QUEUE: append to enqueue, popleft to dequeue (the OLDEST) — the order that makes BFS find the FEWEST steps.

  17. from collections import deque
    queue = deque()
    queue.append("A")
    queue.append("B")
    print("dequeue:", queue.popleft())
    print("dequeue:", queue.popleft())
    print("empty?", len(queue) == 0)

    Output

    dequeue: A
    dequeue: B
    empty? True
    

    popleft removes the oldest item (A before B); len(queue) == 0 tests empty. (A stack's pop would take the NEWEST — wrong for shortest paths.)

  18. BFS explores layer by layer, recording a distance dict; the first time it reaches a cell is via a SHORTEST path.

  19. from collections import deque
    grid = ["...", ".#.", "..."]
    rows = 3
    cols = 3
    start = (0, 0)
    queue = deque()
    queue.append(start)
    distance = {start: 0}
    dr = [-1, 1, 0, 0]
    dc = [0, 0, -1, 1]
    while len(queue) > 0:
        current = queue.popleft()
        direction = 0
        while direction < 4:
            nr = current[0] + dr[direction]
            nc = current[1] + dc[direction]
            if 0 <= nr and nr < rows and 0 <= nc and nc < cols:
                cell = (nr, nc)
                if grid[nr][nc] != "#" and cell not in distance:
                    distance[cell] = distance[current] + 1
                    queue.append(cell)
            direction = direction + 1
    print("distance to (2, 2):", distance[(2, 2)])

    Output

    distance to (2, 2): 4
    
  20. BFS around the wall reaches (2, 2) in 4 steps — the fewest. Using a stack (LIFO) instead could report a longer path.

  21. Put it together: the maze program reads the grid with S/T, BFS-searches from S, and prints the fewest steps to T (or -1).

  22. Try it
    from collections import deque
    import sys
    
    data = sys.stdin.read()
    tokens = data.split()
    rows = int(tokens[0])
    cols = int(tokens[1])
    grid = []
    start = (-1, -1)
    target = (-1, -1)
    r = 0
    while r < rows:
        row = tokens[r + 2]
        grid.append(row)
        c = 0
        while c < cols:
            if row[c] == "S":
                start = (r, c)
            elif row[c] == "T":
                target = (r, c)
            c = c + 1
        r = r + 1
    queue = deque()
    queue.append(start)
    visited = {start}
    distance = {start: 0}
    dr = [-1, 1, 0, 0]
    dc = [0, 0, -1, 1]
    answer = "-1"
    while len(queue) > 0 and answer == "-1":
        current = queue.popleft()
        if current == target:
            answer = str(distance[current])
        else:
            direction = 0
            while direction < 4:
                nr = current[0] + dr[direction]
                nc = current[1] + dc[direction]
                if 0 <= nr and nr < rows and 0 <= nc and nc < cols:
                    next_cell = (nr, nc)
                    if grid[nr][nc] != "#" and next_cell not in visited:
                        visited.add(next_cell)
                        distance[next_cell] = distance[current] + 1
                        queue.append(next_cell)
                direction = direction + 1
    print(answer)
  23. Run the full solver from this unit folder:

    python assets/l2.py < assets/l2/1.in
  24. BFS with a deque: enqueue neighbours with a distance one more than the current; the FIFO order guarantees the first arrival at T is shortest.

    Complexity: O(R*C) — each cell enqueued once.

  25. Lesson 3 — Depth-First Search Reachability

    DFS goes as DEEP as possible before backing up. Recurse into unvisited neighbours, marking a visited set passed as an argument.

  26. adj = {}
    adj[1] = [2, 3]
    adj[2] = [1]
    adj[3] = [1, 4]
    adj[4] = [3]
    visited = set()
    
    def dfs(node, visited):
        visited.add(node)
        i = 0
        while i < len(adj[node]):
            neighbour = adj[node][i]
            if neighbour not in visited:
                dfs(neighbour, visited)
            i = i + 1
    
    dfs(1, visited)
    print("nodes reached:", len(visited))

    Output

    nodes reached: 4
    

    dfs marks each node and recurses into unvisited neighbours; len(visited) counts everything reachable from the start.

  27. After a DFS from the start, the visited set holds EVERY node reachable from it — so "can we reach X?" is just X in visited.

  28. adj = {}
    adj[1] = [2, 3]
    adj[2] = [1]
    adj[3] = [1]
    adj[4] = [5]
    adj[5] = [4]
    visited = set()
    
    def dfs(node, visited):
        visited.add(node)
        i = 0
        while i < len(adj[node]):
            neighbour = adj[node][i]
            if neighbour not in visited:
                dfs(neighbour, visited)
            i = i + 1
    
    dfs(1, visited)
    print(3 in visited)
    print(4 in visited)

    Output

    True
    False
    
  29. This graph has two separate groups; a DFS from 1 fills visited with {1, 2, 3}, so 3 in visited is True but 4 in visited is False — node 4 lives in a different group and is not reachable.

  30. SHORT-CIRCUIT: stop and return True the moment the target is found.

  31. adj = {}
    adj[1] = [2, 3]
    adj[2] = [1]
    adj[3] = [1, 4]
    adj[4] = [3]
    
    def reaches(node, target, visited):
        if node == target:
            return True
        visited.add(node)
        i = 0
        while i < len(adj[node]):
            neighbour = adj[node][i]
            if neighbour not in visited:
                if reaches(neighbour, target, visited):
                    return True
            i = i + 1
        return False
    
    print(reaches(1, 4, set()))

    Output

    True
    

    reaches(1, 4, ...) is True via 1 → 3 → 4; returning as soon as the target is hit avoids exploring the rest.

  32. Put it together: the reachability program reads E edges then start target, and prints YES if target is reachable from start, else NO.

  33. Try it
    import sys
    
    data = sys.stdin.read()
    tokens = data.split()
    edges = int(tokens[0])
    adj = {}
    position = 1
    i = 0
    while i < edges:
        u = int(tokens[position])
        v = int(tokens[position + 1])
        if u not in adj:
            adj[u] = []
        if v not in adj:
            adj[v] = []
        adj[u].append(v)
        adj[v].append(u)
        position = position + 2
        i = i + 1
    start = int(tokens[position])
    target = int(tokens[position + 1])
    visited = set()
    
    def reaches(node, target, visited):
        if node == target:
            return True
        visited.add(node)
        neighbours = adj.get(node, [])
        i = 0
        while i < len(neighbours):
            next_node = neighbours[i]
            if next_node not in visited:
                if reaches(next_node, target, visited):
                    return True
            i = i + 1
        return False
    
    if reaches(start, target, visited):
        print("YES")
    else:
        print("NO")
  34. Run the full solver from this unit folder:

    python assets/l3.py < assets/l3/1.in
  35. Recursive DFS over the adjacency list with a visited set (an argument); short-circuit YES when the target is reached.

    Complexity: O(V + E) — each node and edge visited once.

  36. Traversal Checklist

    (1) Grid → 4 neighbours with dr/dc, guard bounds; (2) graph → adjacency-list dict (both directions for undirected); (3) shortest steps → BFS with a deque (FIFO); (4) reachability / region size → recursive DFS or flood-fill with a visited set passed as an argument; (5) always skip visited nodes to avoid cycles.

    • The offsets (-1,0), (1,0), (0,-1), (0,1) reach a cell's neighbours; guard with 0 <= nr and nr < rows.

    • An undirected edge is added to BOTH endpoints' lists, and len(adj[node]) is the degree.

    • Flood-fill and DFS recurse into unvisited neighbours, with the visited set passed as an argument.

    • BFS uses a deque queue (append, popleft), so the first arrival at a cell is by a shortest path.

    • After a DFS from the start, target in visited tells whether the target is reachable.

  37. You can now traverse grids and graphs to answer region, distance and reachability questions.