Welcome to Part 2! These exercises build toward CCC Question 5 problems - grid traversal, recursion, and graph exploration (DFS/BFS). We'll progress step-by-step from arrays to 2D grids to recursive exploration!
CCC Question 5 typically involves exploring a 2D grid (like a map or maze). Before we dive into complex algorithms, let's master the basics of working with grids!
Grids represent maps, mazes, game boards, and more. Each cell can represent terrain, obstacles, or data. We need to navigate these efficiently!
Grid Example (4x4):
. . # .
. # . .
. . . #
# . . .
Where:
'.' = walkable
'#' = blocked
Creating a 2D Array:
# Method 1: List of lists grid = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] # Method 2: Programmatically rows, cols = 3, 4 grid = [[0 for _ in range(cols)] for _ in range(rows)]
Accessing Elements:
grid = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] # grid[row][col] print(grid[0][0]) # 1 (top-left) print(grid[1][2]) # 6 (middle row, right) print(grid[2][1]) # 8 (bottom row, middle)
Traversing a 2D Array:
# Nested loops - visit every cell for row in range(len(grid)): for col in range(len(grid[0])): print(f"Cell ({row},{col}) = {grid[row][col]}")
The Four Directions:
# Moving up, down, left, right directions = [ (-1, 0), # up (1, 0), # down (0, -1), # left (0, 1) # right ] # To get neighbors of (row, col): for dr, dc in directions: new_row = row + dr new_col = col + dc # Check if valid position before accessing!
Difficulty: Easy
Your Task:
Write a function grid_sum(grid) that:
Hint: Use nested loops to visit each cell!
Examples:
grid1 = [ [1, 2, 3], [4, 5, 6] ] print(grid_sum(grid1)) # Output: 21 grid2 = [ [10, -5], [3, 7], [2, -1] ] print(grid_sum(grid2)) # Output: 16
Difficulty: Easy
Your Task:
Write a function grid_maximum(grid) that:
Examples:
grid1 = [ [3, 7, 2], [9, 1, 5], [4, 8, 6] ] print(grid_maximum(grid1)) # Output: 9 grid2 = [ [-5, -2], [-10, -1], [-3, -7] ] print(grid_maximum(grid2)) # Output: -1
Difficulty: Easy
Your Task:
Write a function count_value(grid, target) that:
Examples:
grid = [ [1, 2, 1], [3, 1, 2], [1, 3, 1] ] print(count_value(grid, 1)) # Output: 5 print(count_value(grid, 3)) # Output: 2 print(count_value(grid, 5)) # Output: 0
Difficulty: Medium
Your Task:
Write a function sum_rows_and_cols(grid) that:
Examples:
grid = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] print(sum_rows_and_cols(grid)) # Output: ([6, 15, 24], [12, 15, 18]) # Row sums: [1+2+3=6, 4+5+6=15, 7+8+9=24] # Col sums: [1+4+7=12, 2+5+8=15, 3+6+9=18] grid2 = [ [1, 1], [2, 2], [3, 3] ] print(sum_rows_and_cols(grid2)) # Output: ([2, 4, 6], [6, 6])
Difficulty: Medium
Your Task:
Write a function find_position(grid, target) that:
NoneExamples:
grid = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] print(find_position(grid, 5)) # Output: (1, 1) print(find_position(grid, 7)) # Output: (2, 0) print(find_position(grid, 10)) # Output: None
Difficulty: Medium
Your Task:
Write a function count_neighbors(grid, row, col, value) that:
Hint: Use the directions pattern from earlier!
Examples:
grid = [ [1, 2, 1], [2, 5, 2], [1, 2, 1] ] print(count_neighbors(grid, 1, 1, 2)) # Output: 4 # Center cell (5) has 4 neighbors that are all 2 print(count_neighbors(grid, 0, 0, 2)) # Output: 1 # Top-left (1) has only right neighbor = 2 print(count_neighbors(grid, 1, 1, 1)) # Output: 0 # Center cell has no neighbors = 1
Difficulty: Medium
Your Task:
Write a function border_sum(grid) that:
Hint: A cell is on the border if its row is 0 or last row, OR its column is 0 or last column. Be careful not to count corner cells twice!
Examples:
grid1 = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] print(border_sum(grid1)) # Output: 40 # Border cells: 1,2,3,4,6,7,8,9 (not 5, it's in the center) # Sum: 1+2+3+4+6+7+8+9 = 40 grid2 = [ [1, 1, 1, 1], [1, 2, 2, 1], [1, 2, 2, 1], [1, 1, 1, 1] ] print(border_sum(grid2)) # Output: 16 # Border is all the 1s (outer ring) grid3 = [ [5, 10], [15, 20] ] print(border_sum(grid3)) # Output: 50 # All cells are on border in a 2x2 grid
Difficulty: Hard
Your Task:
Write a function transpose(grid) that:
Hint: If the original grid is rows Ć cols, the transposed grid will be cols Ć rows. Element at grid[i][j] moves to transposed[j][i].
Visual Example:
Original (3Ć2): Transposed (2Ć3):
[1, 2] [1, 3, 5]
[3, 4] ā [2, 4, 6]
[5, 6]
Examples:
grid1 = [ [1, 2, 3], [4, 5, 6] ] print(transpose(grid1)) # Output: # [ # [1, 4], # [2, 5], # [3, 6] # ] grid2 = [ [1, 2], [3, 4], [5, 6] ] print(transpose(grid2)) # Output: # [ # [1, 3, 5], # [2, 4, 6] # ] grid3 = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] print(transpose(grid3)) # Output: # [ # [1, 4, 7], # [2, 5, 8], # [3, 6, 9] # ] # Notice: diagonals stay in place, everything else swaps!
Recursion = A function that calls itself to solve smaller versions of the same problem.
The Three Rules of Recursion:
Classic Example: Factorial
def factorial(n): # Base case: 0! = 1 if n == 0: return 1 # Recursive case: n! = n Ć (n-1)! return n * factorial(n - 1) # factorial(5) = 5 Ć factorial(4) # = 5 Ć 4 Ć factorial(3) # = 5 Ć 4 Ć 3 Ć factorial(2) # = 5 Ć 4 Ć 3 Ć 2 Ć factorial(1) # = 5 Ć 4 Ć 3 Ć 2 Ć 1 Ć factorial(0) # = 5 Ć 4 Ć 3 Ć 2 Ć 1 Ć 1 = 120
Visual Thinking:
factorial(3)
āā 3 * factorial(2)
ā āā 2 * factorial(1)
ā ā āā 1 * factorial(0)
ā ā ā āā 1 (base case!)
ā ā āā 1 * 1 = 1
ā āā 2 * 1 = 2
āā 3 * 2 = 6
Difficulty: Easy-Medium
Your Task:
Write a recursive function countdown(n) that:
Hint:
def countdown(n): # Base case: when to stop? # Print current number # Recursive case: countdown from n-1
Examples:
countdown(5) # Output: # 5 # 4 # 3 # 2 # 1 countdown(3) # Output: # 3 # 2 # 1
Difficulty: Medium
Your Task:
Write a recursive function recursive_sum(numbers, index) that:
Hint:
def recursive_sum(numbers, index): # Base case: reached end of list? # Recursive case: current element + sum of rest
Examples:
print(recursive_sum([1, 2, 3, 4, 5], 0)) # Output: 15 print(recursive_sum([1, 2, 3, 4, 5], 2)) # Output: 12 (3+4+5) print(recursive_sum([10, 20, 30], 1)) # Output: 50 (20+30)
Now that we understand recursion and grids, we can combine them with Depth-First Search (DFS)!
What is DFS?
How DFS Works:
Starting at S, explore neighbors:
S 1 2 DFS visits: Sā1ā3ā4ā2ā5
. 3 . (goes deep before wide)
. 4 5
DFS on a Grid - The Pattern:
def dfs(grid, row, col, visited): # Base cases (when to STOP): # 1. Out of bounds? if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]): return # 2. Already visited? if (row, col) in visited: return # 3. Is this cell valid? (depends on problem) if grid[row][col] == 0: # Example: 0 is blocked return # Mark as visited visited.add((row, col)) # Recursive case: explore all 4 neighbors dfs(grid, row - 1, col, visited) # up dfs(grid, row + 1, col, visited) # down dfs(grid, row, col - 1, visited) # left dfs(grid, row, col + 1, visited) # right
Key Insight: DFS explores COMPLETELY down one direction before trying another. It's like exploring a maze by always taking the first available path until you hit a dead end, then backtracking.
Difficulty: Medium-Hard
Your Task:
Write a recursive function flood_fill(grid, row, col, new_color) that:
new_colorHint: This is like painting in MS Paint! The fill spreads to all connected cells of the same color.
Algorithm:
1. Store original color at starting position
2. If current cell is out of bounds OR not original color: return
3. Change current cell to new_color
4. Recursively fill all 4 neighbors
Examples:
grid = [ [1, 1, 1], [1, 0, 1], [1, 1, 1] ] result = flood_fill(grid, 0, 0, 2) # Output: # [2, 2, 2], # [2, 0, 2], # [2, 2, 2] # All connected 1s became 2s grid2 = [ [1, 1, 0], [1, 0, 0], [0, 0, 1] ] result2 = flood_fill(grid2, 0, 0, 5) # Output: # [5, 5, 0], # [5, 0, 0], # [0, 0, 1] # Only top-left connected 1s became 5
Difficulty: Hard
Your Task:
Write a function count_regions(grid, target) that:
Hint: Use DFS! For each unvisited target cell, run DFS to mark the entire region, then increment your counter.
Examples:
grid1 = [ [1, 1, 0, 0], [1, 0, 0, 1], [0, 0, 1, 1] ] print(count_regions(grid1, 1)) # Output: 3 # Three separate groups of 1s grid2 = [ [2, 2, 3], [2, 3, 3], [3, 3, 3] ] print(count_regions(grid2, 3)) # Output: 1 # All 3s are connected print(count_regions(grid2, 2)) # Output: 1 # All 2s are connected
Let's solve a complete DFS grid problem together!
Problem:
Given a 2D grid of 1s (land) and 0s (water), count the number of islands. An island is formed by connecting adjacent lands horizontally or vertically (not diagonally).
Example:
grid = [ [1, 1, 0, 0], [1, 0, 0, 1], [0, 0, 1, 1], [0, 0, 0, 0] ] # Output: 3 islands # Island 1: top-left connected 1s # Island 2: single 1 on right # Island 3: bottom-right connected 1s
[1, 1, 0, 0]
[1, 0, 0, 1]
[0, 0, 1, 1]
[0, 0, 0, 0]
Start at (0,0): It's a 1!
- Mark it as visited
- Check neighbors: (0,1) is also 1, (1,0) is also 1
- Keep exploring until all connected 1s are found
- Island count = 1
Continue scanning...
Find unvisited 1 at (1,3): Island count = 2
Find unvisited 1 at (2,2): Island count = 3
This uses DFS recursion:
1. Create a visited array to track explored cells
2. Initialize island_count = 0
3. For each cell in grid:
- If it's land (1) and not visited:
- Run DFS to mark entire island
- Increment island_count
4. Return island_count
DFS function (recursive):
- Mark current cell as visited
- For each of 4 neighbors:
- If neighbor is valid, land, and unvisited:
- Recursively call DFS on neighbor
def count_islands(grid): if not grid: return 0 rows = len(grid) cols = len(grid[0]) visited = [[False] * cols for _ in range(rows)] island_count = 0 def dfs(row, col): # Base case: out of bounds or not land or already visited if (row < 0 or row >= rows or col < 0 or col >= cols or grid[row][col] == 0 or visited[row][col]): return # Mark as visited visited[row][col] = True # Explore all 4 directions dfs(row - 1, col) # up dfs(row + 1, col) # down dfs(row, col - 1) # left dfs(row, col + 1) # right # Scan entire grid for row in range(rows): for col in range(cols): if grid[row][col] == 1 and not visited[row][col]: dfs(row, col) # Explore entire island island_count += 1 return island_count
grid1 = [ [1, 1, 0, 0], [1, 0, 0, 1], [0, 0, 1, 1] ] print(count_islands(grid1)) # Output: 3 grid2 = [ [1, 1, 1], [0, 1, 0], [1, 1, 1] ] print(count_islands(grid2)) # Output: 1 (all connected!)
Difficulty: Hard
Your Task:
Write a recursive function has_path(maze, row, col, visited) that:
True if you can reach the bottom-right corner from (row, col)False if no path existsHint:
Examples:
maze1 = [ [1, 0, 1], [1, 1, 1], [0, 0, 1] ] visited = set() print(has_path(maze1, 0, 0, visited)) # Output: True # Path exists: (0,0)ā(1,0)ā(1,1)ā(1,2)ā(2,2) maze2 = [ [1, 0, 1], [0, 0, 1], [0, 0, 1] ] visited = set() print(has_path(maze2, 0, 0, visited)) # Output: False # No path - blocked by walls
BFS is different from DFS - instead of going deep, it explores level by level!
What is BFS?
How BFS Works:
Starting at S, explore neighbors:
S 1 2 BFS visits: Sā1ā3ā2ā4ā5
. 3 . (level by level, like ripples)
. 4 5
BFS vs DFS:
BFS on a Grid - The Pattern:
from collections import deque def bfs(grid, start_row, start_col): rows, cols = len(grid), len(grid[0]) # Queue stores: (row, col, distance) queue = deque([(start_row, start_col, 0)]) visited = {(start_row, start_col)} while queue: row, col, dist = queue.popleft() # Process closest cells first # Do something with current cell print(f"Visiting ({row}, {col}) at distance {dist}") # Explore all 4 neighbors for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: new_row, new_col = row + dr, col + dc # Check if valid and unvisited if (0 <= new_row < rows and 0 <= new_col < cols and grid[new_row][new_col] != 0 and # Example: 0 is blocked (new_row, new_col) not in visited): visited.add((new_row, new_col)) queue.append((new_row, new_col, dist + 1))
Key Insight: BFS guarantees you find the SHORTEST path because it explores cells in order of increasing distance!
Difficulty: Very Hard (CCC Q5 Level)
Your Task:
Write a function shortest_path_length(maze) that:
BFS Algorithm Template:
from collections import deque def shortest_path_length(maze): rows, cols = len(maze), len(maze[0]) # BFS uses a queue: (row, col, distance) queue = deque([(0, 0, 1)]) # Start at (0,0) with distance 1 visited = {(0, 0)} while queue: row, col, dist = queue.popleft() # Check if reached destination if row == rows - 1 and col == cols - 1: return dist # Explore all 4 neighbors for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: new_row, new_col = row + dr, col + dc # Check if valid and unvisited if (0 <= new_row < rows and 0 <= new_col < cols and maze[new_row][new_col] == 1 and (new_row, new_col) not in visited): visited.add((new_row, new_col)) queue.append((new_row, new_col, dist + 1)) return -1 # No path found
Examples:
maze1 = [ [1, 1, 0], [0, 1, 0], [0, 1, 1] ] print(shortest_path_length(maze1)) # Output: 5 # Path: (0,0)ā(0,1)ā(1,1)ā(2,1)ā(2,2) = 5 cells maze2 = [ [1, 0, 1], [1, 1, 1], [0, 0, 1] ] print(shortest_path_length(maze2)) # Output: 5 # Multiple paths exist, shortest is length 5 maze3 = [ [1, 0], [0, 1] ] print(shortest_path_length(maze3)) # Output: -1 # No path exists
Remember: Grid problems are just 2D array manipulation + recursion/BFS. Master each piece, then combine them! š