Algorithm

Flood Fill

Depth-First Search (DFS) Pattern

Flood Fill

You are given an image as an m x n grid of integer pixel values, a starting position (sr, sc), and a new color.

Starting from the pixel at (sr, sc), change its color and the color of every pixel reachable from it through a chain of 4-directionally adjacent pixels that all share the starting pixel's original color. Pixels of any other color act as walls and stop the spread; so do the image edges.

Return the modified image. If the starting pixel already holds the new color, the image is returned unchanged.

CONSTRAINTS
  • m == image.length, n == image[i].length
  • 1 <= m, n <= 50
  • 0 <= image[i][j], color < 2¹⁶
  • 0 <= sr < m, 0 <= sc < n
  • The fill spreads only up, down, left and right — never diagonally.
EXAMPLE 1
Input: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2
Output: [[2,2,2],[2,2,0],[2,0,1]]
The start pixel holds 1. Six pixels of color 1 form one connected region and all become 2. The 1 at (2,2) also has color 1 but reaches the region only through a corner, and corners do not connect, so it stays 1. Matching the color is not enough — a pixel must be reachable.
EXAMPLE 2
Input: image = [[0,0,0],[0,1,1]], sr = 1, sc = 1, color = 2
Output: [[0,0,0],[0,2,2]]
The start pixel holds 1, so only the connected run of 1s changes. The four 0s are a different color and act as walls, untouched even though they surround the filled area.
EXAMPLE 3
Input: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, color = 0
Output: [[0,0,0],[0,0,0]]
The new color equals the original color, so every pixel that would be repainted already holds the target value and the image is unchanged. This case is dangerous for a naive implementation, which can loop forever.
EXAMPLE 4
Input: image = [[1]], sr = 0, sc = 0, color = 2
Output: [[2]]
A single pixel has no neighbours at all. It is repainted and the fill stops immediately, since every direction runs off the edge.
What should happen if the starting pixel already holds the new color?
Return the image unchanged. This deserves an explicit early check — without one the usual recursive solution never terminates, because a repainted pixel still matches the color being searched for and gets revisited forever.
Does the fill spread diagonally?
No, only in the four orthogonal directions. Two same-colored pixels touching at a corner belong to different regions.
Do pixels elsewhere in the image sharing the original color get repainted?
Only if they are connected to the start through a continuous path of that color. A matching color in an unconnected part of the image is left alone — this is a fill, not a find-and-replace.
May I modify the input image, or should I return a copy?
Modifying in place and returning the same array is standard and uses no extra memory. Confirm it, since the caller may need the original.

This is the paint-bucket tool from any drawing program, and it is the cleanest possible introduction to an idea that runs through a whole family of grid problems: how to visit an entire connected region without visiting anything twice and without missing anything.

Read the rule carefully first, because it has a subtlety. Pixels are repainted if they share the start's color and are reachable from the start through pixels of that color. Both conditions. A pixel of the right color sitting in a different part of the image is not touched. So this is not a search-and-replace over the image — it is a spreading process constrained by connectivity.

Why checking each pixel independently fails

The most literal approach walks the whole image and, for each pixel of the original color, asks whether it connects back to the start.

python
for r, c in all_pixels:
    if image[r][c] == original and connected_to_start(r, c):   # expensive
        image[r][c] = color

The problem is that connected_to_start is not a cheap test — answering it means performing a search, and a search can sweep the entire image. With up to m × n pixels each triggering a search of up to m × n steps, the cost is O((m × n)²).

The waste is easy to see once stated: every pixel in the same region runs its own search over the same territory to rediscover the same fact. They are all connected to each other; establishing that once should be enough for all of them.

The reframe: let the fill spread itself

Turn it around. Instead of standing on each pixel and asking whether it can reach the start, stand on the start and let it reach outward.

Repaint the starting pixel. Then look at its four neighbours; any that still hold the original color are part of the region, so repaint them and repeat from each. Any neighbour holding a different color is a wall, and any position off the grid is a wall too, so the spread stops there naturally.

Every pixel in the region is reached exactly once, and the boundary conditions of the region are discovered as a by-product rather than tested for. One traversal replaces thousands.

Recursion, from zero

The spread is naturally expressed as a function that calls itself, so it is worth being exact about what that means.

A recursive function solves a task by dealing with one small part itself and delegating the remainder to another copy of itself. For it to finish rather than run forever, two things must hold.

The first is a base case — an input handled immediately with no further calls. Here there are two: a position outside the grid, and a pixel whose color is not the original. Both simply return.

The second is progress: each call must leave strictly less work behind. Here, every call repaints its own pixel before delegating, so the count of pixels still holding the original color drops by one every time. A quantity that decreases by one and can never go below zero must eventually stop.

Then the part that feels like cheating but is not. When the function calls itself on the pixel above, do not try to follow that call in your head. Assume it correctly fills everything reachable upward and returns. The justification is induction: the base cases are correct by inspection, and if each call on a strictly smaller amount of remaining work is correct, then a call that handles its own pixel and delegates the four directions is correct too. Trusting the call is the whole technique — attempting to trace all the branches is what makes recursion feel impossible.

Underneath, each call in progress occupies a stack frame holding its arguments and the line to resume at. Frames stack up as the fill pushes outward and unwind as calls return. That is also the memory cost: the deepest chain of calls, which for a snaking region can be the whole image.

python
def flood_fill(image, sr, sc, color):
    original = image[sr][sc]
    if original == color:
        return image                      # already done; also prevents infinite recursion
    rows, cols = len(image), len(image[0])

    def fill(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols or image[r][c] != original:
            return                        # base case: off-image, or a wall
        image[r][c] = color               # repaint BEFORE recursing
        fill(r + 1, c)
        fill(r - 1, c)
        fill(r, c + 1)
        fill(r, c - 1)

    fill(sr, sc)
    return image
Crucial Notethe guard if original == color is not a minor optimisation, it is what stops the function hanging. Without it, imagine filling a region of 1s with the color 1. The first pixel is repainted 1 — no visible change — and still matches original, so when its neighbour recurses back into it, the base case does not fire and it recurses onward again. Two adjacent pixels call each other until the stack overflows. The repaint is what normally marks a pixel as done, and when the new color equals the old one, that mark becomes invisible.
Crucial Notethe repaint happens before the four recursive calls, never after. This is the same mechanism from the other direction. Recurse first and a pixel still holds its original color while its neighbours are being processed, so a neighbour immediately calls back into it, and the pair loops forever. Marking on entry is what makes the progress guarantee real.

Each pixel is entered at most once, since entering it repaints it out of the matching set, and each entry inspects four neighbours. The work is O(m × n) with recursion depth up to O(m × n).

Worked example

Filling from (1,1) with color 2, where the original color is 1:

text
1 1 0
1 1 0
0 0 0
- fill(1,1) — in bounds, holds 1, so repaint it to 2. The grid's centre-left block is now partly filled. Recurse down.
- fill(2,1) — in bounds but holds 0, which is not the original color. Base case, return. That is the wall below.
- Back in (1,1), recurse up. fill(0,1) — holds 1, repaint to 2. From there: down returns immediately, since (1,1) now holds 2 and no longer matches; up runs off the top edge; right hits (0,2) holding 0; left reaches fill(0,0), which holds 1 and is repainted to 2. From (0,0) all four directions are edges or already-repainted pixels, so it returns, and (0,1) returns.
- Back in (1,1), recurse right. fill(1,2) holds 0 — wall, return.
- Back in (1,1), recurse left. fill(1,0) holds 1, repaint to 2. Its neighbours: up is (0,0), already 2, no match; down is (2,0), holds 0; left is off the edge; right is (1,1), already 2. Return.
- fill(1,1) has now made all four calls and returns. Done.

Result: the 2×2 block of 1s is entirely 2s and every 0 is untouched. Four pixels repainted, each entered exactly once, and every attempt to re-enter one was stopped by the color check.

Now look at the third example, filling [[0,0,0],[0,0,0]] from (0,0) with color 0. The guard fires on the first line and the image is returned immediately. Delete that guard and this input hangs — which is why it appears in the test set at all.

The skeleton, and where it goes next

What has been built here is the fundamental connected-region traversal, and the same seven lines reappear across a whole family of problems with only small substitutions. Three slots vary:

- What makes a cell part of the region? Here, matching the original color. Elsewhere: being land, being an 'O', being a height at least as large as the current one.
- How is a cell marked as done? Here, repainting it — the marking and the output are the same act. Elsewhere: sinking a cell to water, writing to a separate visited set, or storing a distance.
- What is reported? Here, nothing; the grid itself is the answer. Return a count instead and you have Max Area of Island. Increment a counter each time you launch a traversal and you have Number of Islands. Launch from every border cell and note what was reached and you have Surrounded Regions.

Depth-first search is used here rather than breadth-first for a specific reason worth internalising: the question asks nothing about distance. Every pixel in the region gets the same treatment regardless of how far it sits from the start, so the ordering guarantee that breadth-first search provides is worthless and its queue is pure overhead. When a problem does ask for a shortest path or a minimum number of steps, that calculus flips entirely.

The one real weakness of the recursive form is stack depth. At 50 × 50 it is never a problem, but the identical code on a 300 × 300 grid can nest 90,000 calls and crash. The fix keeps the same logic and swaps the language's call stack for one you manage:

python
stack = [(sr, sc)]
image[sr][sc] = color
while stack:
    r, c = stack.pop()
    for nr, nc in ((r+1,c), (r-1,c), (r,c+1), (r,c-1)):
        if 0 <= nr < rows and 0 <= nc < cols and image[nr][nc] == original:
            image[nr][nc] = color        # mark on discovery, before pushing
            stack.append((nr, nc))

Same visits, same order of exploration, no recursion limit. Note the mark still happens before the push, for exactly the reason it happened before the recursive call.

Train the reflex: when a problem asks you to do something to a whole connected region, do not test each cell for membership. Start at one member and spread, marking as you enter, and let the region's boundary emerge on its own.

Interactive Strategy Visualization
Simulation Engine

Recursive Propagation

Depth-First Search (DFS) Strategy

Efficiency
O(N)Nodes
Path Count
0Pixels
1
1
1
0
0
1
1
0
0
0
1
0
0
3
3
0
0
3
3
3
0
0
3
3
3
Phase
1/8

"Initializing seed scan. Locating starting pixel at coordinate (1,1)."

Systemic InsightThe Graph Abstraction

Flood fill treats the grid as a professional graph. The algorithm identifies pixels as nodes and establishes connections only between neighbors that share the identical origin state.

O((M × N)²) Connectivity Test Per Pixel
O(M × N) Recursive Depth-First Fill
O(M × N) Iterative Fill With Explicit Stack