Structs & Data Structures · intermediate · ~14 min
Grids are graphs; flood fill explores them.
A grid is secretly a graph: every cell is a vertex linked to its four orthogonal neighbours. Flood fill is just DFS/BFS on that hidden graph — starting from a cell, it spreads to all connected cells of the same kind. It powers island counting, the paint-bucket tool, and connected-region labelling.
Grid/flood-fill problems are ubiquitous: image segmentation and the paint bucket, counting regions in maps or game boards, maze and terrain analysis, and connected-component labelling in computer vision.
Implicit adjacency. Cell (r,c) neighbours (r±1,c) and (r,c±1) — no matrix needed.
Marking. Sink each filled cell (set it to 0) or use a separate visited grid so you never revisit.
Counting vs sizing. Each flood you start is one island; the number of cells a flood consumes is that island's size.
4- vs 8-connectivity. These problems use 4-directional adjacency; diagonals would merge more regions.
/* recursive 4-connected flood fill; sinks each filled cell so it is not revisited */
static int fill(int *g, int rows, int cols, int i, int j) {
if (i < 0 || i >= rows || j < 0 || j >= cols) return 0; /* BOUNDS FIRST */
if (g[i*cols + j] != 1) return 0; /* not land, or already filled */
g[i*cols + j] = 0; /* sink it - no visited array needed */
return 1 + fill(g,rows,cols,i+1,j) + fill(g,rows,cols,i-1,j)
+ fill(g,rows,cols,i,j+1) + fill(g,rows,cols,i,j-1);
}
int count_islands(const int *grid, int rows, int cols) {
int *g = malloc((size_t)rows * cols * sizeof *g); /* work on a COPY */
if (!g) return -1;
for (int k = 0; k < rows*cols; k++) g[k] = grid[k];
int islands = 0;
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
if (g[i*cols + j] == 1) { fill(g,rows,cols,i,j); islands++; }
free(g);
return islands;
}
Key points:
|| short-circuits left to right, so this ordering is what keeps it safe.const on the parameter documents that guarantee.A grid is a graph in disguise: each cell is a vertex joined to its four orthogonal neighbours. Flood fill is DFS/BFS on that implicit graph — it powers count the islands and size the largest island of connected 1-cells, the paint-bucket tool, and region labelling. Work on a copy (or mark cells) so you never revisit, and never connect diagonally unless asked.
#include <stdio.h> static int R=4,Cc=5; static int g[20]={ 1,1,0,0,1, 1,0,0,1,1, 0,0,1,0,0, 1,0,1,1,0 }; static int fill(int i,int j){ if(i<0||i>=R||j<0||j>=Cc||g[iCc+j]!=1) return 0; g[iCc+j]=0; return 1+fill(i+1,j)+fill(i-1,j)+fill(i,j+1)+fill(i,j-1); } int main(void){ int islands=0, biggest=0; for(int i=0;i<R;i++) for(int j=0;j<Cc;j++) if(g[i*Cc+j]==1){ int s=fill(i,j); islands++; if(s>biggest)biggest=s; } printf("islands=%d largest=%d\n", islands, biggest); return 0; }
| Step | Line | What happens |
|---|---|---|
| 1 | scan finds a 1 |
An unvisited land cell — the start of a new island. |
| 2 | fill sinks it |
Sets it to 0 so no later call (or the outer scan) will see it again. |
| 3 | four recursive calls | Up, down, left, right — 4-connectivity. Diagonals are deliberately excluded. |
| 4 | a neighbour off-grid | The bounds check returns 0 before any array access, so no out-of-range read occurs. |
| 5 | fill returns | Its return value is the island's cell count, summed as the recursion unwinds. |
| 6 | outer scan continues | Every remaining 1 starts another island; the number of fills is the island count. |
Connecting cells diagonally when only orthogonal moves are allowed. Mutating the caller's grid instead of a copy. Bounds errors at the grid edges. Forgetting to track the running maximum when sizing.
Compiler errors and warnings:
warning: passing 'const int *' to parameter of type 'int *' — a useful signal that fill mutates and therefore needs the copy.Runtime symptoms:
g[i*cols + j], or is missing a side (a j < 0 omission is easy to miss).cols; using rows works only when the grid is square, which is why square test data hides this bug.rows*cols; use an explicit stack or BFS.Technique: test a 1xN strip, an all-land grid, an all-water grid, and a non-square grid. The non-square case catches stride bugs that square tests cannot.
i < 0 || i >= rows || j < 0 || j >= cols must be fully evaluated before g[i*cols + j]. Relying on the read to "fail" is undefined behaviour, and a negative index reads before the allocation.rows x cols grid the index is i*cols + j. A wrong stride stays inside the allocation, so nothing crashes — the answer is simply wrong. Square test grids hide this; always test rectangular.fill writes to the grid, so either copy the input (as here) or document that the caller's data is consumed. Taking const on the public function and copying internally is the safer contract.rows * cols can overflow int; compute in size_t and validate both dimensions are positive.Concrete uses: The paint-bucket tool in every image editor, connected-component labelling in computer vision, counting distinct regions in satellite or medical imagery, terrain and territory analysis in games, and mine-sweeper-style reveal cascades. The same implicit-graph idea drives maze solving and reachability on tile maps.
Professional best practices:
Beginner:
Intermediate:
visited array when the input grid must be preserved and copying is expensive.1. (Beginner) Count islands. Implement int count_islands(const int *grid, int rows, int cols) on a copy. Example: the demo grid -> 4. Concepts: flood fill, sinking cells.
2. (Beginner) Largest island. Implement int max_island_size(...) using the fill's return value. Example: -> 3. Concepts: sizing a region.
3. (Intermediate) Rectangular grids. Test a 2x5 and a 5x2 grid to prove your stride is cols, not rows. Concepts: stride bugs square tests hide.
4. (Intermediate) Iterative fill. Rewrite with an explicit stack so a 1000x1000 solid grid cannot exhaust the call stack. Concepts: removing recursion depth limits.
A grid is a graph in disguise: each cell is a vertex joined to its four orthogonal neighbours, and flood fill is simply DFS on that implicit graph. Sinking each filled cell to 0 serves as the visited marker, so the number of fills the outer scan starts is the island count and each fill's return value is that island's size. Two rules dominate correctness: check all four bounds before reading the cell (a negative index is an out-of-bounds read, not a harmless miss), and use cols as the row stride — a wrong stride stays inside the allocation and silently corrupts the answer, which square test grids will never reveal. Work on a copy to protect the caller's data, and switch to an iterative fill for large grids since depth can reach the cell count.