DSA sheet · Graphs · Grid traversal (DFS / BFS)

Flood Fill

This is the "paint bucket" tool from any drawing app: click a pixel, and the whole same-coloured patch around it changes colour. It is the simplest grid traversal there is, which makes it the ideal place to learn the recursive grid DFS template: a "false condition" base case, change the cell, then 4 calls. The teacher also shows why the recolouring itself works as the visited marker, and a one-line base case (new colour = old colour) that is easy to skip but turns out to be essential.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Graph basics and grids as graphs

Nodes, edges, and the two kinds of graph input

A graph is nodes joined by edges. Edges can be undirected (a)—(b), directed (a)→(b), or weighted (a)—4—(b). The teacher reminds us that graphs come in two looks:

nodes-and-edges input → adjacency list
n = 3
edges = [[0, 1], [1, 2]]
adj = [[] for _ in range(n)]
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u)          # undirected
print(adj)                    # [[1], [0, 2], [1]]
            (r-1, c)
               ↑
 (r, c-1)  ←  (r, c)  →  (r, c+1)
               ↓
            (r+1, c)       diagonals are NOT neighbours here

Visited, connected components, BFS and DFS

Because neighbours point back at each other ((r, c) is a child of (r+1, c) and the other way round), a traversal must remember which cells it has already handled. Otherwise it bounces between two cells forever. That memory is the visited marker. All the cells you can reach from a start cell form its connected component. In Flood Fill, that's exactly the patch we must repaint. To reach them we can use DFS (recursion: go deep in one direction first) or BFS (a queue: spread ring by ring).

Grid toolkit: bounds and directions

neighbours of a cell with dr/dc
rows, cols = 3, 3
dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
r, c = 0, 2                       # top-right corner
inside = []
for i in range(4):
    nr, nc = r + dr[i], c + dc[i]
    if 0 <= nr < rows and 0 <= nc < cols:
        inside.append((nr, nc))
print(inside)                     # [(1, 2), (0, 1)]

Part A · Flood fill with DFS

LeetCode 733

1The question in simple words

You get an image as an m × n grid of integers. Each number is a pixel's colour. You also get a start pixel (sr, sc) (source row, source column) and a new color.

  1. Remember the start pixel's colour, the original colour.
  2. Repaint the start pixel with color.
  3. Repaint every pixel that touches a repainted pixel up, down, left or right (not diagonally) and has the original colour. Keep spreading.
  4. Stop when no more such pixels exist. Return the image.
input: sr = 1, sc = 1, color = 2
      c0 c1 c2
 r0 [ 1, 1, 1 ]
 r1 [ 1,1, 0 ]     ← start (1,1), original = 1
 r2 [ 1, 0, 1 ]
output
      c0 c1 c2
 r0 [ 2, 2, 2 ]
 r1 [ 2, 2, 0 ]
 r2 [ 2, 0, 1 ]    ← never reached

The bottom-right 1 has the original colour, but it only touches the patch diagonally (through (1,1)). Its up and left neighbours are 0s. So the paint can't reach it, and it stays 1.

2What the constraints tell us

Doubt: the teacher reads the value limit as "1016". Would that need a long?
→ The statement says 216, not 1016, and her conclusion ("it's small, a normal int is enough") matches 216. A value of 1016 would not fit in a 32-bit int (max about 2.1 × 10⁹). It's a slip in reading; the conclusion is right.

3Intuition: spilled paint

Pour paint on the start pixel. It runs into every neighbouring pixel that has the same old colour, then from those into their same-coloured neighbours, and so on. It stops at any different colour (a wall) and at the image border. The question doesn't ask for speed, it just says "repaint everything reachable". So any traversal that reaches every cell of the patch works, BFS or DFS. The teacher starts with DFS.

4Building the logic step by step

Step 1: read the original colour first

original = image[sr][sc]. We need it because a pixel may only be repainted if its colour equals the original. A 0 next to the start must not become 2. And once we start painting, the start cell itself will no longer show the original colour, so we must save it before we change anything.

Step 2: the base case: new colour = original colour

The teacher asks: what if the start is already colour 1 and we're told to paint it 1? Repainting 1s into 1s changes nothing, so return the image straight away.

Doubt 1: is this check only a time-saver?
→ It's much more than that: without it, the code never stops. Our "visited" trick (step 4) relies on a painted cell no longer looking like the original. If color == original, painting a cell leaves it equal to the original. Its neighbour then calls back into it, which calls back again… forever (in Python: RecursionError). The test file shows this crash. So this line is required.

Step 3: what does the DFS function need?

The teacher's rule: pass whatever changes between calls, or whatever the function can't otherwise know. That means the row and column (they change every call), the image, the new color, and the original colour (so every cell can be compared with it).

Step 4: no visited array needed, because the paint is the mark

Normally (in "90% of graph problems", as the teacher says) we keep a visited grid so we don't enter a cell twice. Here the work itself does the marking:

Her example: from (1,1) we go up into (0,1) and paint it. From (0,1), the cell below, (1,1), is a child again. But (1,1) is already 2, not 1, so we don't go there. No extra memory is needed.

Step 5: write the "false condition" first

In recursive DFS the teacher prefers to check when we must not continue, and return at once. Reasons to refuse a cell:

  1. r < 0 or r >= rows: off the top or bottom (≥, because indexes stop at rows − 1);
  2. c < 0 or c >= cols: off the left or right;
  3. image[r][c] != original: a wall (a different colour), or a cell we already painted.

If none of these is true, the cell is inside and has the original colour, so paint it.

Doubt 2: does the order inside the condition matter?
→ Yes. The bounds checks must come before image[r][c]. Python's or stops at the first True, so if r is −1 we return before ever reading image[-1].

Step 6: paint, then 4 calls

After painting, visit the 4 children with 4 calls: (r+1, c), (r, c+1), (r-1, c), (r, c-1). We call blindly; any call into a bad cell just hits the false condition and returns. The teacher shows the same 4 calls can be written as a loop over dr/dc. Both are shown below.

Doubt 3: why does DFS dive into the up cell before finishing the left cell of (1,1)?
→ That's what "depth first" means. A BFS would first handle all 4 children of (1,1). DFS takes the first child that works and finishes its whole region before coming back for the next child of (1,1). By the time it comes back, the left cell is often already painted, and the call just returns.

Step 7: change the input or copy it?

The teacher's interview note: we paint the given image directly. If the interviewer doesn't want the input changed, make a copy first ([row[:] for row in image]) and paint the copy. Ask.

5Approach steps

  1. original = image[sr][sc]. If original == color → return the image.
  2. Call dfs(sr, sc), then return the image.
  3. dfs(r, c): if out of bounds or image[r][c] != original → return.
  4. Set image[r][c] = color.
  5. Call dfs on down, right, up, left.

6Code (Python)

Flood Fill with DFS (4 recursive calls)
import sys

class Solution:
    def floodFill(self, image, sr, sc, color):
        sys.setrecursionlimit(10000)           # a 50 x 50 patch can go 2500 deep
        original = image[sr][sc]
        if original == color:                  # nothing to change (and avoids endless calls)
            return image
        self.dfs(image, sr, sc, color, original)
        return image

    def dfs(self, image, r, c, color, original):
        rows, cols = len(image), len(image[0])
        if r < 0 or r >= rows or c < 0 or c >= cols or image[r][c] != original:
            return                             # the "false condition"
        image[r][c] = color                    # paint = mark visited
        self.dfs(image, r + 1, c, color, original)   # down
        self.dfs(image, r, c + 1, color, original)   # right
        self.dfs(image, r - 1, c, color, original)   # up
        self.dfs(image, r, c - 1, color, original)   # left
Flood Fill with DFS (the same thing using dr / dc)
import sys

class Solution:
    def floodFill(self, image, sr, sc, color):
        sys.setrecursionlimit(10000)
        original = image[sr][sc]
        if original == color:
            return image
        rows, cols = len(image), len(image[0])
        dr = [-1, 1, 0, 0]                     # up, down, left, right
        dc = [0, 0, -1, 1]

        def dfs(r, c):
            if r < 0 or r >= rows or c < 0 or c >= cols or image[r][c] != original:
                return
            image[r][c] = color
            for i in range(4):
                dfs(r + dr[i], c + dc[i])

        dfs(sr, sc)
        return image

7Code line by line

linewhat it means
original = image[sr][sc]Save the colour we're replacing, before anything is painted.
if original == color: return imageNothing would change, and without this the painted-cell-as-visited trick breaks (endless recursion).
self.dfs(image, sr, sc, color, original)Start the paint at the given pixel.
rows, cols = len(image), len(image[0])Grid size, used for the bounds check.
if r < 0 or r >= rows or c < 0 or c >= colsOff the image → stop. Checked first so we never read outside.
or image[r][c] != original: returnA wall (another colour) or an already painted cell → stop.
image[r][c] = colorPaint it. From now on it no longer matches original, so it counts as visited.
4 calls: (r+1,c) (r,c+1) (r-1,c) (r,c-1)Spread to the 4 side neighbours. Bad ones return instantly.
for i in range(4): dfs(r + dr[i], c + dc[i])The same 4 calls written as a loop over the direction arrays.

8Dry run with the call stack

Example 1, start (1,1), original 1, color 2. Call order inside each cell: down, right, up, left (the first code block). "✗" = the call returns at once.

stepcallwhat happenscall stack after (bottom → top)painted so far
1dfs(1,1)inside, 1 = original → paint(1,1)(1,1)
2down (2,1), right (1,2)both are 0 → ✗(1,1)(1,1)
3up dfs(0,1)1 → paint(1,1) (0,1)+ (0,1)
4(0,1) down (1,1)already 2 → ✗ (the paint acts as visited)(1,1) (0,1)
5(0,1) right dfs(0,2)1 → paint. Its 4 calls: (1,2)=0 ✗, (0,3) out ✗, (−1,2) out ✗, (0,1)=2 ✗ → return(1,1) (0,1)+ (0,2)
6(0,1) up (−1,1)out ✗(1,1) (0,1)
7(0,1) left dfs(0,0)1 → paint(1,1) (0,1) (0,0)+ (0,0)
8(0,0) down dfs(1,0)1 → paint(1,1) (0,1) (0,0) (1,0)+ (1,0)
9(1,0) down dfs(2,0)1 → paint. Its calls: (3,0) out, (2,1)=0, (1,0)=2, (2,−1) out → all ✗ → return(1,1) (0,1) (0,0) (1,0)+ (2,0)
10rest of (1,0)right (1,1)=2 ✗, up (0,0)=2 ✗, left out ✗ → return(1,1) (0,1) (0,0)
11rest of (0,0)right (0,1)=2 ✗, up out, left out → return(1,1) (0,1)
12(0,1) doneall 4 calls finished → return(1,1)
13(1,1) left (1,0)already 2 → ✗. (1,1) is done → return(empty)6 cells
step 3
dfs(1,1)dfs(0,1)
step 9: deepest point
dfs(1,1)dfs(0,1)dfs(0,0)dfs(1,0)dfs(2,0)
step 13
dfs(1,1) → left is 2 → done
after step 5
1 2 2
1 2 0
1 0 1
after step 9
2 2 2
2 2 0
2 0 1
final (the corner 1 was never reached)
2 2 2
2 2 0
2 0 1

Notice step 13: the left neighbour of the start was painted long ago through the "up" branch. When the start finally tries "left", the call bounces off. That's the DFS order from Doubt 3.

9Complexity & remember

Remember Save original → if it equals color, return.
DFS: false condition first (out of bounds or not original → return), then paint, then 4 calls.
The paint is the visited mark.

Part B · Flood fill with BFS

The teacher says this question works just as well with BFS ("also very simple") and leaves it for the next videos. Here is what changes. Everything about the colours, the base case and the paint-as-visited idea stays the same.

1–2Question & constraints

Same as Part A. A small bonus: BFS has no recursion, so the depth limit doesn't matter.

3Intuition

The paint spreads in rings: first the start, then its 4 neighbours, then theirs. A queue keeps the cells that are painted but whose neighbours haven't been checked yet.

4What changes from DFS

5Approach steps

  1. original = image[sr][sc]. If it equals color, return.
  2. Paint (sr, sc), push it.
  3. While the queue isn't empty: pop (r, c). For each of the 4 directions: if (nr, nc) is inside and equals original → paint it, push it.
  4. Return the image.

6Code (Python)

Flood Fill with BFS
from collections import deque

class Solution:
    def floodFill(self, image, sr, sc, color):
        original = image[sr][sc]
        if original == color:
            return image
        rows, cols = len(image), len(image[0])
        dr = [-1, 1, 0, 0]
        dc = [0, 0, -1, 1]
        image[sr][sc] = color
        queue = deque([(sr, sc)])
        while queue:
            r, c = queue.popleft()
            for i in range(4):
                nr, nc = r + dr[i], c + dc[i]
                if 0 <= nr < rows and 0 <= nc < cols and image[nr][nc] == original:
                    image[nr][nc] = color          # paint when pushed
                    queue.append((nr, nc))
        return image

7Line by line (only the new lines)

linewhat it means
image[sr][sc] = color queue = deque([(sr, sc)])Paint the start and let it wait for its neighbours to be checked.
r, c = queue.popleft()The oldest painted cell, so the paint spreads ring by ring.
if 0 <= nr < rows and 0 <= nc < cols and image[nr][nc] == original:The DFS "false condition", flipped into a "go" condition and checked before pushing.
image[nr][nc] = color queue.append((nr, nc))Paint now (marks it visited), and expand it later.

8Dry run (directions up, down, left, right)

stepcell poppedneighbours checkedqueue afterimage after
0-paint start (1,1)[(1,1)]1 1 1 / 1 2 0 / 1 0 1
1(1,1)up (0,1) ✓ · down (2,1)=0 · left (1,0) ✓ · right (1,2)=0[(0,1), (1,0)]1 2 1 / 2 2 0 / 1 0 1
2(0,1)up out · down 2 · left (0,0) ✓ · right (0,2) ✓[(1,0), (0,0), (0,2)]2 2 2 / 2 2 0 / 1 0 1
3(1,0)up (0,0)=2 (already painted) · down (2,0) ✓ · left out · right 2[(0,0), (0,2), (2,0)]2 2 2 / 2 2 0 / 2 0 1
4–6(0,0), (0,2), (2,0)nothing new: all neighbours are 2, 0, or outside[ ]2 2 2 / 2 2 0 / 2 0 1 ✓

Step 3 shows "paint when pushed" at work: (0,0) was pushed by (0,1) and is already 2, so (1,0) doesn't push it again.

9Complexity & remember

RememberBFS = check before pushing, paint when pushing. Same base case, same answer as DFS.

Part C · Revision page

DFS (teacher's code)BFS
visited markerthe new colour itself (painted cells ≠ original)
must-have base caseif original == color: return image
where the check happensat the top of each call ("false condition" → return)before pushing a neighbour
when a cell is paintedon entering the callwhen it is pushed
time / spaceO(m·n) / O(m·n) stackO(m·n) / O(m·n) queue
Python noteraise the recursion limitno limit issue
grid problem so farvisited is stored as…
Number of Islandsland '1' turned into water '0'
Flood Filloriginal colour turned into the new colour
If you remember only 5 lines 1. Grid = graph: a cell's children are its 4 side neighbours, computed from indexes.
2. Save original = image[sr][sc] before painting anything.
3. If original == color, return the image unchanged (otherwise it loops forever).
4. DFS: out of bounds or not original → return; else paint and make 4 calls.
5. The paint marks visited, so no extra grid is needed.
Mistakes to avoid ✗ skipping the original == color check → endless recursion
✗ reading image[r][c] before the bounds check (and image[-1] wraps around in Python)
✗ comparing with image[sr][sc] inside DFS instead of the saved original (the start is already repainted)
✗ going diagonally: only up, down, left, right
✗ r > rows instead of r >= rows in the bounds check
test it yourself (paste under any solution above)
s = Solution()
print(s.floodFill([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 2))   # [[2, 2, 2], [2, 2, 0], [2, 0, 1]]
print(s.floodFill([[0, 0, 0], [0, 0, 0]], 0, 0, 0))              # [[0, 0, 0], [0, 0, 0]]
print(s.floodFill([[5]], 0, 0, 7))                               # [[7]]
print(s.floodFill([[1, 2], [2, 1]], 0, 0, 3))                    # [[3, 2], [2, 1]]
print(s.floodFill([[4, 4], [4, 4]], 1, 0, 9))                    # [[9, 9], [9, 9]]

Based on this video: Flood Fill | Graph