Facebook Pixel

3905. Multi Source Flood Fill

LeetCode ↗

Problem Description

You are given two integers n and m representing the number of rows and columns of a grid, respectively.

You are also given a 2D integer array sources, where sources[i] = [ri, ci, colori] indicates that the cell (ri, ci) is initially colored with colori. All other cells are initially uncolored and represented as 0.

At each time step, every currently colored cell spreads its color to all adjacent uncolored cells in the four directions: up, down, left, and right. All spreads happen simultaneously.

If multiple colors reach the same uncolored cell at the same time step, the cell takes the color with the maximum value.

The process continues until no more cells can be colored.

Return a 2D integer array representing the final state of the grid, where each cell contains its final color.

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

How We Pick the Algorithm

Why BFS?

This problem maps to BFS through a short path in the full flowchart.

Graph?yesSmallconstraints?noBFS

A multi-source BFS simultaneously expands from all starting cells.

Open in Flowchart
Show step-by-step reasoning

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The grid can be treated as a graph, where each cell is a node and edges connect adjacent cells (up, down, left, right).

Is it a tree?

  • No: The grid contains cycles, so it is not a tree.

Is the problem related to directed acyclic graphs?

  • No: The grid is undirected and contains cycles, so it is not a DAG.

Is the problem related to shortest paths?

  • Yes: The color spreads outward level by level, where each cell is reached in the minimum number of time steps from its source.

Is the graph Weighted?

  • No: Every spread to an adjacent cell takes exactly one time step, so all edges have the same weight.

Conclusion: The flowchart suggests using a BFS approach. Since multiple sources spread simultaneously and each cell is colored at the earliest time step it can be reached, this is a multi-source BFS that processes the grid level by level, resolving ties by taking the maximum color value.

Intuition

The key observation is that color spreads outward one ring at a time, and every cell takes the color of whichever source reaches it first. This "spreading simultaneously from many starting points" behavior is exactly what multi-source BFS models. By starting the BFS from all source cells at once, every cell naturally gets colored at the earliest time step it can be reached, which matches the rule that each cell holds the color from the nearest source.

The only twist is the tie-breaking rule: if several colors reach the same uncolored cell at the same time step, the cell takes the maximum color value. A standard BFS that processes neighbors one by one would not handle this correctly, because the order in which we visit neighbors could decide the winner arbitrarily. To respect the "all spreads happen simultaneously" condition, we must process the grid level by level (one time step at a time), and within a single level, collect all candidate colors for each newly reached cell before committing.

This leads to the idea of buffering each level. For the current frontier of colored cells, we look at all their uncolored neighbors and record, for each such neighbor, the maximum color trying to reach it during this step. We use a hash table vis keyed by cell coordinates so that if multiple sources target the same cell, we keep only the largest color via max. Once the entire level is examined, we finalize those cells by writing their colors into the answer grid and pushing them into the queue as the next frontier.

Because a cell is only colored once (we skip any neighbor that already has a color), every cell is processed a constant number of times, so the whole grid fills up efficiently. We keep repeating this level-by-level expansion until the queue becomes empty, meaning no more uncolored cells can be reached, at which point the grid has reached its final state.

Pattern Learn more about Breadth-First Search patterns.

Solution Approach

We use multi-source BFS to simulate the spreading process.

We start by creating the answer grid ans of size n x m, filled with 0 to represent uncolored cells. We define a queue q to hold the cells that are currently spreading their color. Initially, q is set to sources, and we write each source's color into ans so the starting cells are marked.

To explore the four directions conveniently, we use the trick dirs = (-1, 0, 1, 0, -1). By taking consecutive pairs with pairwise(dirs), we get the offsets (-1, 0), (0, 1), (1, 0), (0, -1), which correspond to up, right, down, and left.

The BFS proceeds level by level inside a while q loop, where each iteration represents one time step:

  1. We create a hash table vis (a defaultdict(int)) to record, for the current time step, every newly reachable uncolored cell and the maximum color trying to reach it.
  2. For each cell (r, c, color) in the queue, we look at its four neighbors (x, y). We skip a neighbor if it falls outside the grid (not 0 <= x < n or not 0 <= y < m) or if it is already colored (ans[x][y] is non-zero).
  3. For a valid uncolored neighbor, we update vis[(x, y)] = max(vis[(x, y)], color). This enforces the rule that when several colors reach the same cell at the same step, the largest color wins, regardless of processing order.
  4. After scanning the whole frontier, we clear q. Then for each entry (x, y) -> color in vis, we set ans[x][y] = color and push (x, y, color) into q to form the next frontier.

We repeat this until q becomes empty, meaning no more uncolored cells can be reached. At that point ans holds the final colors, and we return it.

Each cell is finalized exactly once (we never revisit colored cells), and processing it inspects a constant number of neighbors, so the time complexity is O(n * m) and the space complexity is O(n * m) for the answer grid, the queue, and the vis table.

Example Walkthrough

Let's trace through a small example to see how the multi-source BFS handles both the spreading and the tie-breaking rule.

Input:

  • n = 3, m = 3
  • sources = [[0, 0, 1], [2, 2, 3], [0, 2, 5]]

Initial Setup

We create ans filled with 0, then write each source's color into its cell. The queue starts as q = [(0,0,1), (2,2,3), (0,2,5)].

ans:                 q (frontier):
1 0 5                (0,0,1)
0 0 0                (2,2,3)
0 0 0                (0,2,5)

Time Step 1 — process the whole frontier into a fresh vis.

For each colored cell, inspect its four neighbors and record the max color trying to reach each uncolored cell:

  • (0,0,1) → reaches (0,1) and (1,0).
    • vis[(0,1)] = max(0, 1) = 1
    • vis[(1,0)] = max(0, 1) = 1
  • (2,2,3) → reaches (1,2) and (2,1).
    • vis[(1,2)] = max(0, 3) = 3
    • vis[(2,1)] = max(0, 3) = 3
  • (0,2,5) → reaches (0,1) and (1,2).
    • vis[(0,1)] = max(1, 5) = 5 ← color 5 beats the earlier 1!
    • vis[(1,2)] = max(3, 5) = 5 ← color 5 beats the earlier 3!

Notice how (0,1) and (1,2) are each reached by two colors in the same step. Because we buffer candidates and take the max, the larger value wins regardless of the order we scanned the frontier.

Now commit vis to ans and rebuild q:

ans:                 q (new frontier):
1 5 5                (0,1,5)
1 0 3                (1,0,1)
0 3 3                (1,2,5)
                     (2,1,3)

Time Step 2 — only (1,1) remains uncolored.

  • (0,1,5) → reaches (1,1): vis[(1,1)] = max(0, 5) = 5
  • (1,0,1) → reaches (1,1): vis[(1,1)] = max(5, 1) = 5
  • (1,2,5) → reaches (1,1): vis[(1,1)] = max(5, 5) = 5
  • (2,1,3) → reaches (1,1): vis[(1,1)] = max(5, 3) = 5

All four directions converge on (1,1) simultaneously, and color 5 wins the tie. Commit and rebuild q:

ans:                 q (new frontier):
1 5 5                (1,1,5)
1 5 3
0 3 3

Wait — (2,0) is still uncolored. It was a neighbor of (1,0) but at step 2 we only processed the step-2 frontier. Let's continue.

Time Step 3 — process (1,1,5).

  • (1,1,5) → neighbors (0,1), (1,0), (1,2), (2,1) are all already colored, so nothing new is added from it.

But (2,0) was actually reachable from (1,0,1) back at step 2. Re-checking step 2: (1,0,1) also reaches (2,0), giving vis[(2,0)] = 1. So after step 2 the frontier also included (2,0,1):

ans:                 q after step 2:
1 5 5                (1,1,5)
1 5 3                (2,0,1)
1 3 3

At step 3, (1,1,5) and (2,0,1) have only colored neighbors, so vis is empty. The queue clears and becomes empty.

Termination

With q empty, no more cells can be colored. The final grid is:

1 5 5
1 5 3
1 3 3

Key takeaways from the trace:

  • Multi-source BFS colors every cell at the earliest reachable time step.
  • The vis buffer is essential: cells like (0,1), (1,2), and (1,1) were targeted by multiple colors in the same step, and taking max guaranteed the largest color won — independent of frontier processing order.
  • Each cell is finalized exactly once, then skipped as a colored neighbor thereafter, keeping the work at O(n * m).

Solution Implementation

1from collections import defaultdict
2from itertools import pairwise
3
4
5class Solution:
6    def colorGrid(self, n: int, m: int, sources: list[list[int]]) -> list[list[int]]:
7        # Result grid initialized with 0 (uncolored cells)
8        result = [[0] * m for _ in range(n)]
9
10        # Current BFS frontier; start from all source cells at once
11        frontier = sources
12
13        # Direction offsets paired via pairwise:
14        # (-1, 0), (0, 1), (1, 0), (0, -1) -> up, right, down, left
15        directions = (-1, 0, 1, 0, -1)
16
17        # Paint all source cells with their given colors first
18        for row, col, color in frontier:
19            result[row][col] = color
20
21        # Multi-source BFS: expand layer by layer
22        while frontier:
23            # For each newly reachable cell, keep the largest color
24            # among all sources that touch it in this layer (tie-break by max)
25            next_colors = defaultdict(int)
26
27            for row, col, color in frontier:
28                for delta_row, delta_col in pairwise(directions):
29                    next_row, next_col = row + delta_row, col + delta_col
30
31                    # Skip out-of-bounds cells or already colored cells
32                    if (
33                        not 0 <= next_row < n
34                        or not 0 <= next_col < m
35                        or result[next_row][next_col]
36                    ):
37                        continue
38
39                    # Record the maximum color reaching this neighbor
40                    next_colors[(next_row, next_col)] = max(
41                        next_colors[(next_row, next_col)], color
42                    )
43
44            # Build the next frontier and commit the chosen colors to the grid
45            frontier = []
46            for (next_row, next_col), color in next_colors.items():
47                frontier.append((next_row, next_col, color))
48                result[next_row][next_col] = color
49
50        return result
51
1class Solution {
2    /**
3     * Performs a multi-source BFS flood fill on an n x m grid.
4     * Each source paints the grid with its own color, expanding one ring per round.
5     * When multiple sources reach the same empty cell in the same round,
6     * the cell takes the maximum color value among the competing sources.
7     *
8     * @param n       number of rows
9     * @param m       number of columns
10     * @param sources array of sources, each as {row, col, color}
11     * @return the fully colored grid
12     */
13    public int[][] colorGrid(int n, int m, int[][] sources) {
14        // Resulting grid; 0 means "not yet colored".
15        int[][] ans = new int[n][m];
16
17        // Current BFS frontier; each element is {row, col, color}.
18        List<int[]> queue = new ArrayList<>();
19
20        // Direction offsets for up, right, down, left (paired as dirs[i], dirs[i+1]).
21        int[] dirs = {-1, 0, 1, 0, -1};
22
23        // Initialize the grid and the frontier with all the sources.
24        for (int[] source : sources) {
25            int row = source[0];
26            int col = source[1];
27            int color = source[2];
28            ans[row][col] = color;
29            queue.add(new int[] {row, col, color});
30        }
31
32        // Expand the frontier one ring at a time until no new cells can be colored.
33        while (!queue.isEmpty()) {
34            // For this round, map each candidate cell (encoded as a key) to the
35            // maximum color that wants to occupy it.
36            Map<Long, Integer> candidates = new HashMap<>();
37
38            for (int[] current : queue) {
39                int row = current[0];
40                int col = current[1];
41                int color = current[2];
42
43                // Try to expand into each of the four neighbors.
44                for (int i = 0; i < 4; i++) {
45                    int nextRow = row + dirs[i];
46                    int nextCol = col + dirs[i + 1];
47
48                    // Only consider in-bounds cells that are still uncolored.
49                    if (nextRow >= 0 && nextRow < n
50                            && nextCol >= 0 && nextCol < m
51                            && ans[nextRow][nextCol] == 0) {
52                        // Encode (nextRow, nextCol) as a single long key.
53                        long key = (long) nextRow * m + nextCol;
54                        // Keep the largest color competing for this cell.
55                        candidates.put(key, Math.max(candidates.getOrDefault(key, 0), color));
56                    }
57                }
58            }
59
60            // Build the next frontier by committing the winning colors.
61            queue.clear();
62            for (Map.Entry<Long, Integer> entry : candidates.entrySet()) {
63                long key = entry.getKey();
64                int row = (int) (key / m);
65                int col = (int) (key % m);
66                int color = entry.getValue();
67
68                // Paint the cell and add it to the next round's frontier.
69                ans[row][col] = color;
70                queue.add(new int[] {row, col, color});
71            }
72        }
73
74        return ans;
75    }
76}
77
1class Solution {
2public:
3    vector<vector<int>> colorGrid(int n, int m, vector<vector<int>>& sources) {
4        // Result grid initialized to 0 (uncolored).
5        vector<vector<int>> ans(n, vector<int>(m, 0));
6
7        // BFS frontier: each entry holds {row, col, color}.
8        vector<array<int, 3>> queue;
9
10        // Direction offsets for the 4 neighbors (up, right, down, left).
11        // Read consecutive pairs: (-1,0), (0,1), (1,0), (0,-1).
12        int dirs[] = {-1, 0, 1, 0, -1};
13
14        // Seed the BFS with all source cells and color them immediately.
15        for (auto& source : sources) {
16            ans[source[0]][source[1]] = source[2];
17            queue.push_back({source[0], source[1], source[2]});
18        }
19
20        // Multi-source BFS, processed level by level so cells reached at the
21        // same distance are resolved together.
22        while (!queue.empty()) {
23            // For the current level, record the best (largest) candidate color
24            // for each uncolored neighbor cell, keyed by a flattened index.
25            unordered_map<long long, int> visited;
26
27            for (auto& current : queue) {
28                int row = current[0], col = current[1], color = current[2];
29
30                // Explore the 4 orthogonal neighbors.
31                for (int i = 0; i < 4; i++) {
32                    int nextRow = row + dirs[i];
33                    int nextCol = col + dirs[i + 1];
34
35                    // Only consider in-bounds, still-uncolored cells.
36                    if (nextRow >= 0 && nextRow < n &&
37                        nextCol >= 0 && nextCol < m &&
38                        ans[nextRow][nextCol] == 0) {
39                        // Flatten 2D coordinates into a single key.
40                        long long key = (long long) nextRow * m + nextCol;
41
42                        // Keep the highest color among competing sources.
43                        if (color > visited[key]) {
44                            visited[key] = color;
45                        }
46                    }
47                }
48            }
49
50            // Prepare the next BFS level from the resolved neighbor cells.
51            queue.clear();
52            for (auto const& [key, color] : visited) {
53                int row = key / m;
54                int col = key % m;
55
56                // Commit the winning color and enqueue for further expansion.
57                ans[row][col] = color;
58                queue.push_back({row, col, color});
59            }
60        }
61
62        return ans;
63    }
64};
65
1/**
2 * Performs a multi-source BFS flood fill on a grid.
3 * Each empty cell is colored by the nearest source. When multiple sources
4 * reach a cell simultaneously (same distance), the larger color value wins.
5 *
6 * @param rows    - Number of rows in the grid.
7 * @param cols    - Number of columns in the grid.
8 * @param sources - Array of sources, each as [row, col, color].
9 * @returns The fully colored grid.
10 */
11function colorGrid(rows: number, cols: number, sources: number[][]): number[][] {
12    // Initialize the result grid with all cells set to 0 (uncolored).
13    const grid: number[][] = Array.from({ length: rows }, () => Array(cols).fill(0));
14
15    // Initialize the BFS frontier with a deep copy of the source cells.
16    let frontier: number[][] = sources.map((source) => [...source]);
17
18    // Direction offsets for the four neighbors: up, right, down, left.
19    // Consecutive pairs (dirs[i], dirs[i + 1]) form (dr, dc).
20    const dirs: number[] = [-1, 0, 1, 0, -1];
21
22    // Paint all source cells with their respective colors.
23    for (const [row, col, color] of frontier) {
24        grid[row][col] = color;
25    }
26
27    // Expand the frontier outward level by level until no new cells remain.
28    while (frontier.length > 0) {
29        // Track candidate colors for each newly reachable cell at this level.
30        // Key: "row,col"; Value: the maximum color reaching that cell.
31        const candidates: Map<string, number> = new Map();
32
33        for (const [row, col, color] of frontier) {
34            // Explore the four orthogonal neighbors.
35            for (let i = 0; i < 4; i++) {
36                const nextRow = row + dirs[i];
37                const nextCol = col + dirs[i + 1];
38
39                // Only consider in-bounds cells that are still uncolored.
40                if (
41                    nextRow >= 0 &&
42                    nextRow < rows &&
43                    nextCol >= 0 &&
44                    nextCol < cols &&
45                    grid[nextRow][nextCol] === 0
46                ) {
47                    const key = `${nextRow},${nextCol}`;
48                    // Keep the largest color among all sources reaching this cell.
49                    candidates.set(key, Math.max(candidates.get(key) || 0, color));
50                }
51            }
52        }
53
54        // Build the next frontier from the resolved candidate cells.
55        frontier = [];
56        for (const [key, color] of candidates.entries()) {
57            const [nextRow, nextCol] = key.split(',').map(Number);
58            grid[nextRow][nextCol] = color;
59            frontier.push([nextRow, nextCol, color]);
60        }
61    }
62
63    return grid;
64}
65

Time and Space Complexity

  • Time Complexity: O(n × m). This is a multi-source BFS that fills the grid layer by layer. Each cell (x, y) is added to the vis dictionary and later processed at most once, because once a cell's color is set in ans, the check ans[x][y] prevents it from being revisited. For every visited cell we examine its 4 neighbors, which is a constant amount of work. Therefore, across all BFS rounds, the total number of cell processings is bounded by the total number of cells, giving O(n × m), where n and m are the number of rows and columns in the grid, respectively.

  • Space Complexity: O(n × m). The answer grid ans requires O(n × m) space. The queue q and the vis dictionary at any point store the cells of the current BFS frontier, which in the worst case can hold up to O(n × m) cells. Hence the overall space complexity is O(n × m).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Committing colors to the grid before the entire frontier is processed

A very common mistake is to write each neighbor's color into result immediately as you discover it, instead of buffering the choices in next_colors first:

# WRONG: commit as soon as we see a neighbor
for row, col, color in frontier:
    for delta_row, delta_col in pairwise(directions):
        next_row, next_col = row + delta_row, col + delta_col
        if not 0 <= next_row < n or not 0 <= next_col < m or result[next_row][next_col]:
            continue
        result[next_row][next_col] = color          # committed too early!
        next_frontier.append((next_row, next_col, color))

Why it's wrong: The problem requires that when multiple colors reach the same cell at the same time step, the cell takes the maximum color. If you commit immediately, the first color that happens to reach the cell wins (because result[next_row][next_col] becomes truthy and later, larger colors are skipped by the result[...] check). The result then depends on the order of cells in frontier, which is incorrect.

Solution: Use a per-layer buffer (next_colors) and apply max while scanning, then commit only after the whole frontier is examined — exactly as the reference code does:

next_colors = defaultdict(int)
for row, col, color in frontier:
    for delta_row, delta_col in pairwise(directions):
        ...
        next_colors[(next_row, next_col)] = max(next_colors[(next_row, next_col)], color)

# Commit only after the full layer is scanned
frontier = []
for (next_row, next_col), color in next_colors.items():
    frontier.append((next_row, next_col, color))
    result[next_row][next_col] = color

Pitfall 2: Forgetting to mark source cells before the BFS loop, or allowing them to be overwritten

If you skip the initial loop that paints the sources into result:

for row, col, color in sources:
    result[row][col] = color

then during the first BFS layer, a source cell may be treated as uncolored by a neighboring source and get overwritten (or re-added to the frontier). Worse, two adjacent sources would try to recolor each other.

Solution: Paint all sources into result before entering the while frontier loop. The result[next_row][next_col] truthiness check then correctly prevents any already-colored source from being revisited.

⚠️ Edge note: This assumes all colori values are strictly positive (non-zero), since 0 is the sentinel for "uncolored." If a source could legitimately have color 0, the truthiness check (result[...] and defaultdict(int)'s default of 0) would break the logic, and you'd need a separate boolean visited grid to distinguish "colored with 0" from "uncolored."


Pitfall 3: Relying on color values being positive in defaultdict(int)

next_colors is a defaultdict(int), so a missing key defaults to 0, and max(0, color) works only because every real color is > 0.

Solution: If colors can be 0 or negative, replace the max accumulation with an explicit presence check:

key = (next_row, next_col)
if key not in next_colors or color > next_colors[key]:
    next_colors[key] = color

and use a dedicated visited 2D boolean array instead of relying on result[...] truthiness to detect colored cells.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

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

Load More