Facebook Pixel

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:

  1. Start at the pixel image[sr][sc] and change its color to the given color
  2. 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
  3. Change all these connected pixels to the new color
  4. Continue this process recursively for each newly colored pixel, finding and coloring their neighbors that match the original color
  5. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Reachable from our starting point through adjacent moves (up, down, left, right)
  2. 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:

  1. Changes the current pixel's color: image[i][j] = color
  2. 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:

  1. 0 <= x < len(image): The row index is within bounds
  2. 0 <= y < len(image[0]): The column index is within bounds
  3. image[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 Evaluator

Example 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 to color
  • 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More