733. Flood Fill
Problem Description
You have an m x n
grid representing an image where each cell image[i][j]
contains an integer representing a pixel's color value. You need to perform a flood fill operation starting from a specific pixel at position (sr, sc)
and change it to a new color value.
The flood fill operation works like the paint bucket tool in image editing software:
- Start at the pixel
image[sr][sc]
and change its color to the givencolor
- Find all pixels that are directly connected to this pixel (up, down, left, right - not diagonally) that have the same original color as the starting pixel
- Change all these connected pixels to the new
color
- Continue this process recursively for each newly colored pixel, finding and coloring their neighbors that match the original color
- Stop when no more pixels with the original color can be reached
The goal is to change the color of all pixels that:
- Are connected to the starting pixel through a path of adjacent pixels (horizontally or vertically)
- Share the same color as the original color of the starting pixel
Return the modified image after completing the flood fill operation.
For example, if you start at a pixel with color value 1, all connected pixels with color value 1 should be changed to the new color, but pixels with different colors or pixels not connected to the starting pixel remain unchanged.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The image grid can be viewed as a graph where each pixel is a node, and edges connect adjacent pixels (horizontally and vertically). We need to traverse connected components of pixels with the same color.
Is it a tree?
- No: The grid structure is not a tree. It's a 2D grid where pixels can form cycles (e.g., four pixels forming a square all with the same color would create a cycle).
Is the problem related to directed acyclic graphs?
- No: The problem doesn't involve directed edges or acyclic properties. The flood fill explores bidirectionally between adjacent pixels.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between pixels. Instead, we need to visit all connected pixels with the same color.
Does the problem involve connectivity?
- Yes: The core of flood fill is finding all pixels connected to the starting pixel that share the same original color. This is essentially finding a connected component in the graph.
Does the problem have small constraints?
- Yes: Image grids typically have reasonable dimensions (m Γ n), making recursive approaches feasible without stack overflow concerns.
DFS/backtracking?
- Yes: DFS is perfect for exploring all connected pixels recursively. Starting from the initial pixel, we recursively visit all adjacent pixels with the same color and change them.
Conclusion: The flowchart leads us to use DFS (Depth-First Search) for the flood fill algorithm. DFS naturally explores connected components by recursively visiting neighbors, which is exactly what flood fill requires - changing all connected pixels of the same color.
Intuition
Think of the flood fill operation like spilling paint on a connected surface - the paint naturally flows to all connected areas of the same material. In our case, the "same material" means pixels with the same color value.
The key insight is recognizing that we need to explore all pixels that are:
- Reachable from our starting point through adjacent moves (up, down, left, right)
- Have the same color as our starting pixel's original color
This is a classic connected component problem. When we change a pixel's color, we need to check its four neighbors. If any neighbor has the original color, we should change it too and then check its neighbors. This recursive nature of "change current pixel, then check and process neighbors" naturally leads us to DFS.
Why DFS over BFS? Both would work, but DFS is more intuitive here because:
- We can implement it recursively with cleaner code
- We don't need to track the level or distance (which BFS excels at)
- We just need to visit all connected pixels, and DFS does this efficiently by going as deep as possible before backtracking
The critical edge case to handle is when the new color equals the original color. If we don't check for this, we'd get stuck in infinite recursion - we'd keep "recoloring" the same pixels with the same color forever. That's why the solution checks if oc != color
before starting the DFS.
The algorithm naturally terminates when we've visited all pixels in the connected component because:
- Changed pixels now have a different color than the original
- Our DFS only continues for pixels matching the original color
- This creates a natural boundary that stops the recursion
Learn more about Depth-First Search and Breadth-First Search patterns.
Solution Approach
The solution implements a DFS approach to perform the flood fill operation. Let's break down the implementation step by step:
Initial Setup:
- Store the original color of the starting pixel:
oc = image[sr][sc]
- Check if the original color is different from the target color:
if oc != color
- If they're the same, no work needs to be done - return the image as is
DFS Function:
The core of the solution is the recursive dfs(i, j)
function that:
- Changes the current pixel's color:
image[i][j] = color
- Explores all four adjacent directions (up, down, left, right)
Direction Traversal:
The solution uses a clever pattern with dirs = (-1, 0, 1, 0, -1)
and pairwise
:
pairwise(dirs)
generates pairs:(-1, 0)
,(0, 1)
,(1, 0)
,(0, -1)
- These pairs represent direction vectors: up, right, down, left
- For each direction
(a, b)
, calculate the new position:x, y = i + a, j + b
Boundary and Color Checking: Before recursing to a neighbor pixel, three conditions must be met:
0 <= x < len(image)
: The row index is within bounds0 <= y < len(image[0])
: The column index is within boundsimage[x][y] == oc
: The neighbor pixel has the original color
If all conditions are satisfied, recursively call dfs(x, y)
to process that neighbor.
Why This Works:
- The recursion naturally handles the "spreading" effect of flood fill
- By checking
image[x][y] == oc
before recursing, we ensure we only process pixels with the original color - Once a pixel is changed to the new color, it won't be processed again (preventing infinite recursion)
- The DFS explores as far as possible in each direction before backtracking, efficiently covering all connected pixels
Time Complexity: O(m Γ n)
where m and n are the dimensions of the image, as we might need to visit every pixel once in the worst case.
Space Complexity: O(m Γ n)
for the recursion stack in the worst case when all pixels need to be filled.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the flood fill algorithm:
Initial Setup:
image = [[1, 1, 1], [1, 1, 0], [1, 0, 1]] Starting position: (sr, sc) = (1, 1) New color: 2
Step 1: Initialize
- Original color at position (1,1):
oc = image[1][1] = 1
- Since
oc (1) β color (2)
, we proceed with the DFS
Step 2: Start DFS at (1,1)
- Change
image[1][1] = 2
- Image becomes:
[[1, 1, 1], [1, 2, 0], [1, 0, 1]]
- Check 4 neighbors:
- Up (0,1): Has value 1 (matches original) β
- Right (1,2): Has value 0 (doesn't match) β
- Down (2,1): Has value 0 (doesn't match) β
- Left (1,0): Has value 1 (matches original) β
Step 3: DFS to (0,1)
- Change
image[0][1] = 2
- Image becomes:
[[1, 2, 1], [1, 2, 0], [1, 0, 1]]
- Check neighbors:
- Up: Out of bounds β
- Right (0,2): Has value 1 β
- Down (1,1): Has value 2 (already changed) β
- Left (0,0): Has value 1 β
Step 4: DFS to (0,2)
- Change
image[0][2] = 2
- Image becomes:
[[1, 2, 2], [1, 2, 0], [1, 0, 1]]
- Check neighbors: None have original color 1 that are reachable
Step 5: Backtrack and DFS to (0,0)
- Change
image[0][0] = 2
- Image becomes:
[[2, 2, 2], [1, 2, 0], [1, 0, 1]]
- Check neighbors:
- Right (0,1): Already value 2 β
- Down (1,0): Has value 1 β
Step 6: DFS to (1,0)
- Change
image[1][0] = 2
- Image becomes:
[[2, 2, 2], [2, 2, 0], [1, 0, 1]]
- Check neighbors:
- Up (0,0): Already value 2 β
- Right (1,1): Already value 2 β
- Down (2,0): Has value 1 β
Step 7: DFS to (2,0)
- Change
image[2][0] = 2
- Final image:
[[2, 2, 2], [2, 2, 0], [2, 0, 1]]
- No more neighbors with original color 1
Result: All pixels with value 1 that were connected to position (1,1) have been changed to 2. The pixels with value 0 act as barriers, and the pixel at (2,2) with value 1 remains unchanged because it's not connected to our starting point through pixels of the same color.
Solution Implementation
1class Solution:
2 def floodFill(
3 self, image: List[List[int]], sr: int, sc: int, color: int
4 ) -> List[List[int]]:
5 """
6 Performs flood fill algorithm starting from position (sr, sc) with new color.
7
8 Args:
9 image: 2D grid of integers representing pixel colors
10 sr: starting row index
11 sc: starting column index
12 color: new color to fill with
13
14 Returns:
15 Modified image after flood fill
16 """
17
18 # Store the original color at starting position
19 original_color = image[sr][sc]
20
21 # Only proceed if new color is different from original color
22 # This prevents infinite recursion
23 if original_color == color:
24 return image
25
26 # Get dimensions of the image
27 rows = len(image)
28 cols = len(image[0])
29
30 def dfs(row: int, col: int) -> None:
31 """
32 Depth-first search to fill connected pixels with same original color.
33
34 Args:
35 row: current row index
36 col: current column index
37 """
38 # Fill current pixel with new color
39 image[row][col] = color
40
41 # Define four directions: up, right, down, left
42 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
43
44 # Explore all four adjacent cells
45 for delta_row, delta_col in directions:
46 new_row = row + delta_row
47 new_col = col + delta_col
48
49 # Check if new position is valid and has the original color
50 if (0 <= new_row < rows and
51 0 <= new_col < cols and
52 image[new_row][new_col] == original_color):
53 # Recursively fill the adjacent cell
54 dfs(new_row, new_col)
55
56 # Start flood fill from the given starting position
57 dfs(sr, sc)
58
59 return image
60
1class Solution {
2 // Instance variables to avoid passing parameters repeatedly
3 private int[][] image; // The image matrix to be modified
4 private int originalColor; // The original color at starting position
5 private int newColor; // The new color to fill with
6
7 // Direction array for exploring 4 adjacent cells (up, right, down, left)
8 // Using pairs: (dirs[i], dirs[i+1]) represents (row offset, column offset)
9 private final int[] directions = {-1, 0, 1, 0, -1};
10
11 /**
12 * Performs flood fill algorithm starting from position (sr, sc)
13 * @param image - 2D array representing the image
14 * @param sr - starting row index
15 * @param sc - starting column index
16 * @param color - new color to fill the connected region
17 * @return modified image after flood fill
18 */
19 public int[][] floodFill(int[][] image, int sr, int sc, int color) {
20 // Store the original color at starting position
21 originalColor = image[sr][sc];
22
23 // If original color equals new color, no change needed
24 if (originalColor == color) {
25 return image;
26 }
27
28 // Initialize instance variables
29 this.image = image;
30 this.newColor = color;
31
32 // Start DFS from the starting position
33 dfs(sr, sc);
34
35 return image;
36 }
37
38 /**
39 * Depth-first search to fill all connected cells with the same original color
40 * @param row - current row index
41 * @param col - current column index
42 */
43 private void dfs(int row, int col) {
44 // Fill current cell with new color
45 image[row][col] = newColor;
46
47 // Explore all 4 adjacent directions
48 for (int k = 0; k < 4; k++) {
49 // Calculate next cell coordinates
50 int nextRow = row + directions[k];
51 int nextCol = col + directions[k + 1];
52
53 // Check if next cell is valid and has the original color
54 if (nextRow >= 0 && nextRow < image.length &&
55 nextCol >= 0 && nextCol < image[0].length &&
56 image[nextRow][nextCol] == originalColor) {
57 // Recursively fill the adjacent cell
58 dfs(nextRow, nextCol);
59 }
60 }
61 }
62}
63
1class Solution {
2public:
3 vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
4 int rows = image.size();
5 int cols = image[0].size();
6 int originalColor = image[sr][sc];
7
8 // If the starting pixel already has the target color, no need to fill
9 if (originalColor == color) {
10 return image;
11 }
12
13 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
14 // Using a single array where consecutive pairs represent (dx, dy)
15 const int directions[5] = {-1, 0, 1, 0, -1};
16
17 // Lambda function for depth-first search traversal
18 // Uses C++23 deducing this feature for recursive lambda
19 auto dfs = [&](this auto&& dfs, int row, int col) -> void {
20 // Fill current cell with new color
21 image[row][col] = color;
22
23 // Explore all 4 adjacent cells
24 for (int k = 0; k < 4; ++k) {
25 int nextRow = row + directions[k];
26 int nextCol = col + directions[k + 1];
27
28 // Check if the adjacent cell is within bounds and has the original color
29 if (nextRow >= 0 && nextRow < rows &&
30 nextCol >= 0 && nextCol < cols &&
31 image[nextRow][nextCol] == originalColor) {
32 // Recursively fill the adjacent cell
33 dfs(nextRow, nextCol);
34 }
35 }
36 };
37
38 // Start flood fill from the given starting position
39 dfs(sr, sc);
40
41 return image;
42 }
43};
44
1/**
2 * Performs flood fill on a 2D image starting from a given pixel
3 * @param image - 2D array representing the image
4 * @param sr - Starting row index
5 * @param sc - Starting column index
6 * @param color - New color to fill
7 * @returns Modified image after flood fill
8 */
9function floodFill(image: number[][], sr: number, sc: number, color: number): number[][] {
10 // Get image dimensions
11 const rows: number = image.length;
12 const cols: number = image[0].length;
13
14 // Store the original color of the starting pixel
15 const originalColor: number = image[sr][sc];
16
17 // If the original color is the same as the new color, no change needed
18 if (originalColor === color) {
19 return image;
20 }
21
22 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
23 // Using compressed format: [dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, sentinel]
24 const directions: number[] = [-1, 0, 1, 0, -1];
25
26 /**
27 * Depth-first search to fill connected pixels with the same original color
28 * @param row - Current row index
29 * @param col - Current column index
30 */
31 const dfs = (row: number, col: number): void => {
32 // Fill current pixel with new color
33 image[row][col] = color;
34
35 // Explore all 4 adjacent directions
36 for (let k = 0; k < 4; k++) {
37 // Calculate next position using direction vectors
38 const nextRow: number = row + directions[k];
39 const nextCol: number = col + directions[k + 1];
40
41 // Check if next position is valid and has the original color
42 if (nextRow >= 0 && nextRow < rows &&
43 nextCol >= 0 && nextCol < cols &&
44 image[nextRow][nextCol] === originalColor) {
45 // Recursively fill the adjacent pixel
46 dfs(nextRow, nextCol);
47 }
48 }
49 };
50
51 // Start flood fill from the given starting position
52 dfs(sr, sc);
53
54 return image;
55}
56
Time and Space Complexity
Time Complexity: O(m Γ n)
where m
is the number of rows and n
is the number of columns in the image.
In the worst case, the flood fill algorithm needs to visit every pixel in the image. This happens when all pixels have the same color as the starting pixel and need to be changed. The DFS traversal visits each pixel at most once (since we change the color when visiting, preventing revisits), resulting in O(m Γ n)
time complexity.
Space Complexity: O(m Γ n)
where m
is the number of rows and n
is the number of columns in the image.
The space complexity is dominated by the recursive call stack in the DFS traversal. In the worst case, when all pixels need to be filled and form a long chain (like a spiral pattern), the recursion depth could reach O(m Γ n)
. This happens because each recursive call adds a frame to the call stack, and we might need to go through all pixels before backtracking begins.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Infinite Recursion When Original Color Equals New Color
The Problem: One of the most common mistakes is forgetting to check if the original color is the same as the new color before starting the flood fill. Without this check, the algorithm will infinitely recurse because:
- The pixel at
(sr, sc)
gets changed tocolor
- The DFS checks neighbors and finds they have the "original color"
- But since original color = new color, already-processed pixels still match the condition
- The algorithm revisits the same pixels repeatedly, causing stack overflow
Incorrect Implementation:
def floodFill(self, image, sr, sc, color):
original_color = image[sr][sc]
# Missing the check: if original_color == color: return image
def dfs(row, col):
image[row][col] = color
for dr, dc in [(-1,0), (0,1), (1,0), (0,-1)]:
new_row, new_col = row + dr, col + dc
if (0 <= new_row < len(image) and
0 <= new_col < len(image[0]) and
image[new_row][new_col] == original_color):
dfs(new_row, new_col) # Infinite recursion!
dfs(sr, sc)
return image
The Solution: Always check if the original color differs from the target color before starting:
original_color = image[sr][sc] if original_color == color: return image # No work needed, prevents infinite recursion
Pitfall 2: Using a Visited Set Unnecessarily
The Problem:
Some developers instinctively add a visited
set to track processed pixels, similar to other graph traversal problems. While this works, it's unnecessary overhead for flood fill because:
- Once we change a pixel's color, it no longer matches the original color
- The color change itself acts as the "visited" marker
- Adding a visited set increases space complexity without benefit
Overcomplicated Implementation:
def floodFill(self, image, sr, sc, color):
original_color = image[sr][sc]
if original_color == color:
return image
visited = set() # Unnecessary!
def dfs(row, col):
if (row, col) in visited: # Extra check not needed
return
visited.add((row, col))
image[row][col] = color
for dr, dc in [(-1,0), (0,1), (1,0), (0,-1)]:
new_row, new_col = row + dr, col + dc
if (0 <= new_row < len(image) and
0 <= new_col < len(image[0]) and
image[new_row][new_col] == original_color):
dfs(new_row, new_col)
dfs(sr, sc)
return image
The Solution:
Trust that the color change prevents revisiting. The condition image[new_row][new_col] == original_color
naturally excludes already-processed pixels since they now have the new color.
Pitfall 3: Modifying the Condition Check Order
The Problem: Checking the color condition before boundary checks can cause index out of bounds errors:
Incorrect Order:
# Wrong: color check before boundary check if (image[new_row][new_col] == original_color and # IndexError possible! 0 <= new_row < rows and 0 <= new_col < cols): dfs(new_row, new_col)
The Solution: Always validate boundaries before accessing array elements:
# Correct: boundary check first if (0 <= new_row < rows and 0 <= new_col < cols and image[new_row][new_col] == original_color): dfs(new_row, new_col)
Python's short-circuit evaluation ensures the array access only happens after confirming valid indices.
Which of these properties could exist for a graph but not a tree?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!