Flood Fill
Problem
Given a 2D integer array image representing an image, a starting pixel (sr, sc), and a color, return the image after performing a flood fill on the starting pixel, changing the color of all connected pixels with the same original color to the new color.
- m == image.length
- n == image[i].length
- 1 ≤ m, n ≤ 50
- 0 ≤ image[i][j], color < 2¹⁶
- 0 ≤ sr < m
- 0 ≤ sc < n
Example
image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2[[2,2,2],[2,2,0],[2,0,1]]We begin at pixel (1,1). Its original color is 1, so our goal is to recolor the entire region of 1s that is connected to (1,1) using only up, down, left, and right moves. First, we recolor (1,1) from 1 to 2. This is important for two reasons: it applies the requested change, and it also marks the pixel as already processed so we do not loop back to it later. Next, we check the four neighbors of (1,1): - Up at (0,1) has color 1, which matches the original color, so it belongs to the same reachable region and we recolor it to 2. - Down at (2,1) has color 0, which does not match the original color, so it is not part of the region we are filling and it must remain unchanged. - Left at (1,0) has color 1, which matches the original color, so it is part of the reachable region and we recolor it to 2. - Right at (1,2) has color 0, which does not match the original color, so it is not filled. From the newly recolored pixels, we repeat the same process: for every pixel we recolor to 2, we again inspect its up, down, left, and right neighbors. Whenever a neighbor is inside the grid and still has the original color 1, we recolor it to 2 as well. This continues until there are no more reachable neighbors with color 1. The pixel (2,2) stays as 1 even though it is a 1, because it cannot be reached from (1,1) by moving only up, down, left, and right through 1s. It is only diagonally adjacent, and diagonal adjacency does not count for flood fill in this problem.
Approach
Straightforward Solution
A naive approach would scan the entire grid and recolor every pixel whose value equals the starting color. This is inefficient because it touches many pixels that are unrelated to the starting area. More importantly, it is incorrect because it would also recolor pixels with the same value that are separated by barriers of other colors and are not reachable from (sr, sc) using only up, down, left, and right moves.
Core Observation
Flood fill is about recoloring a single “region” of pixels. The region is defined by two rules: you can only move up, down, left, or right, and you can only move through pixels that have the same original color as the start pixel. The algorithm’s job is to visit every reachable pixel that satisfies those rules and recolor it.
Path to Optimal
Recognizing the problem as a graph traversal on a grid, the optimal approach uses DFS or BFS starting from the source pixel. This traversal visits only the connected pixels with the original color, recoloring them in-place. This avoids unnecessary work and ensures correctness.
Optimal Approach
Use DFS starting at (sr, sc). For each pixel, if it is within bounds and matches the original color, recolor it and recursively visit its 4-directional neighbors. This approach guarantees all connected pixels are recolored exactly once, achieving O(m*n) time in the worst case.
Time
O(m * n)
Each pixel is visited at most once during the DFS traversal. The grid has m rows and n columns, so the worst-case time is proportional to the total number of pixels.
Space
O(m * n)
In the worst case, the recursion stack can grow to the size of the entire grid if all pixels are connected and need recoloring. This is auxiliary space used by the DFS recursion.
Pattern Spotlight
DFS (Grid Connected Component Traversal)
When recoloring or marking connected regions in a grid, use DFS or BFS to traverse only the connected component starting from the source, ensuring efficient and correct updates without scanning unrelated areas.
Solution
| 1 | class Solution: |
| 2 | def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]: |
| 3 | m, n = len(image), len(image[0]) |
| 4 | source = image[sr][sc] |
| 5 | if source == color: |
| 6 | return image |
| 7 | |
| 8 | directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] |
| 9 | |
| 10 | def is_valid(r, c): |
| 11 | return 0 <= r < m and 0 <= c < n |
| 12 | |
| 13 | def dfs(r, c): |
| 14 | if not is_valid(r, c) or image[r][c] != source: |
| 15 | return |
| 16 | image[r][c] = color |
| 17 | for dr, dc in directions: |
| 18 | dfs(r + dr, c + dc) |
| 19 | |
| 20 | dfs(sr, sc) |
| 21 | return image |
Step-by-Step Solution
Capture Image Dimensions and Source Color for Reference
| 3 | m, n = len(image), len(image[0]) |
| 4 | source = image[sr][sc] |
Objective
To store the dimensions of the image and the original color of the starting pixel for boundary and color checks during traversal.
Key Insight
Knowing the image size allows for efficient boundary checks to prevent invalid accesses. Capturing the source color upfront enables the algorithm to identify which pixels belong to the connected component that needs recoloring. This setup is essential for correctness and efficiency.
Interview Quick-Check
Core Logic
Storing image dimensions and source color upfront avoids repeated computations and clarifies the traversal conditions.
Common Pitfalls & Bugs
Forgetting to capture the source color before any recoloring can cause incorrect behavior if the starting pixel's color is the same as the new color.
Return Early if Source Color Equals Target Color
| 5 | if source == color: |
| 6 | return image |
Objective
To avoid unnecessary work by returning the image immediately if the starting pixel's color is already the target color.
Key Insight
If the source color matches the new color, no changes are needed. Without this check, the DFS would recurse infinitely or unnecessarily recolor pixels, causing inefficiency or stack overflow. This early exit is a critical optimization and correctness safeguard.
Interview Quick-Check
Core Logic
Early return prevents infinite recursion and redundant work when the source and target colors are identical.
Define Valid Neighbor Directions for 4-Directional Traversal
| 8 | directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] |
Objective
To specify the relative coordinate offsets representing the four possible directions (up, down, left, right) for neighbor exploration.
Key Insight
Explicitly defining directions as coordinate offsets simplifies neighbor traversal logic and makes the DFS code cleaner and less error-prone. This abstraction allows uniform iteration over neighbors without duplicating code.
Interview Quick-Check
Core Logic
Using a directions list abstracts neighbor exploration, enabling concise and maintainable recursive calls.
Common Pitfalls & Bugs
Forgetting to include all four directions or including invalid directions can cause incomplete or incorrect traversal.
Implement Boundary Check Helper to Validate Coordinates
| 10 | def is_valid(r, c): |
| 11 | return 0 <= r < m and 0 <= c < n |
Objective
To create a helper function that returns whether given coordinates are within the image boundaries.
Key Insight
Separating boundary checks into a helper function improves code readability and reduces duplication. It centralizes the logic for coordinate validation, making the DFS function cleaner and easier to maintain.
Interview Quick-Check
Core Logic
A dedicated boundary check function ensures all neighbor coordinates are validated consistently before processing.
Common Pitfalls & Bugs
Neglecting boundary checks leads to index errors or invalid memory access.
Recursively Traverse and Recolor Connected Pixels Using DFS
| 13 | def dfs(r, c): |
| 14 | if not is_valid(r, c) or image[r][c] != source: |
| 15 | return |
| 16 | image[r][c] = color |
| 17 | for dr, dc in directions: |
| 18 | dfs(r + dr, c + dc) |
Objective
To perform a depth-first search starting from the current pixel, recoloring it if it matches the source color, and recursively visiting all valid neighbors.
Key Insight
DFS explores the entire connected component by visiting each pixel exactly once. Recoloring upon visiting prevents revisiting the same pixel, effectively marking it as visited. Recursing over all four directions ensures complete coverage of the connected region. This approach guarantees correctness and efficiency by avoiding redundant work.
Interview Quick-Check
Core Logic
DFS recolors the current pixel and recursively explores all valid neighbors with the original color, ensuring full connected component traversal.
State & Boundaries
The base case returns when the pixel is out of bounds or does not match the source color, preventing infinite recursion.
Common Pitfalls & Bugs
Failing to recolor before recursion or not checking the pixel color can cause infinite loops or incorrect recoloring.
Initiate DFS from Starting Pixel and Return Modified Image
| 20 | dfs(sr, sc) |
| 21 | return image |
Objective
To start the DFS traversal from the given starting pixel and return the updated image after flood fill completion.
Key Insight
Calling DFS from the source pixel triggers the recursive recoloring of the entire connected component. Returning the image after traversal provides the final modified state as required by the problem.
Interview Quick-Check
Core Logic
Starting DFS at (sr, sc) ensures the entire connected component is recolored.
State & Boundaries
Returning the image after DFS completes provides the final flood-filled image.
Line Analysis
This solution has 4 Critical lines interviewers watch for.
image[r][c] = color
Recolor the current pixel with the target color.
This line is the heart of the flood fill; recoloring the pixel both updates the image and acts as a visited marker, preventing infinite recursion and ensuring the connected component is correctly transformed.
if source == color:
Check if the source color is the same as the target color.
If source == color, DFS would not make progress because pixels never change to mark them as visited, which can lead to runaway recursion. Returning early avoids that and avoids wasted work.
if not is_valid(r, c) or image[r][c] != source:
Return if the current pixel is out of bounds or does not match the source color.
This base case prevents infinite recursion and ensures only pixels in the connected component are recolored.
dfs(r + dr, c + dc)
Recursively call DFS on the neighboring pixel.
Recursive calls propagate the flood fill to all connected pixels, enabling depth-first traversal of the component.
source = image[sr][sc]
Capture the original color of the starting pixel.
Storing the source color upfront is essential to identify which pixels belong to the connected component that needs recoloring, ensuring correctness.
return image
Return the original image immediately if no recoloring is needed.
Early return avoids redundant traversal and recoloring, ensuring efficiency and preventing stack overflow.
dfs(sr, sc)
Start DFS traversal from the starting pixel.
Initiating DFS here triggers the entire flood fill process, ensuring all connected pixels are recolored.
def is_valid(r, c):
Define a helper function to check if coordinates are within image bounds.
Centralizing boundary checks prevents index errors and makes the DFS code cleaner and more maintainable.
return 0 <= r < m and 0 <= c < n
Return whether the given coordinates are valid within the image.
This boolean check is the gatekeeper for safe access to image pixels during traversal.
return
Terminate the current DFS call without further recursion.
Returning here stops traversal along invalid or irrelevant paths, maintaining correctness and efficiency.
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
Define the four possible directions for neighbor traversal.
Explicitly listing directions simplifies neighbor exploration logic and ensures consistent 4-directional traversal.
def dfs(r, c):
Define the DFS function to recolor connected pixels.
Encapsulating the recursive traversal logic in a helper function organizes the code and manages recursion state effectively.
for dr, dc in directions:
Iterate over all four neighbor directions.
Looping over directions enables systematic exploration of all adjacent pixels, ensuring complete traversal of the connected component.
m, n = len(image), len(image[0])
Store the number of rows and columns in the image.
Caching the image dimensions enables efficient boundary checks during traversal and avoids repeated calls to len(), improving readability and performance.
return image
Return the modified image after flood fill completion.
Returning the updated image provides the final result as required by the problem statement.
Test Your Understanding
Why must the algorithm check if the current pixel's color matches the original source color before recoloring and recursing?
Because only pixels connected 4-directionally with the same original color should be recolored. Checking prevents recoloring pixels of different colors and avoids infinite recursion by not revisiting already recolored pixels.
Related Problems
DFS pattern
Don't just read it. Drill it.
Reconstruct Flood Fill from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Flood Fill drill