Algorithms with C# · Lesson 28

Flood fill with boundary and revisit rules

Mark cells when queued to visit a connected region exactly once.

How it works

Flood fill visits the connected region containing a starting cell and changes cells with the same original value. The sample uses four-direction connectivity: up, down, left, and right. Diagonal contact does not connect cells. Starting at the upper-left one therefore changes three cells, while the other ones remain untouched.

The queue represents pending cells. A cell is changed to the replacement value before it is enqueued, so a second neighboring cell cannot enqueue it again as an unchanged original cell. Marking only when dequeued could create duplicate pending work and complicate the changed-count invariant.

The replacement must differ from the original marker for this implementation's visited-state strategy to work. The early return handles an identical replacement without searching. An alternative design could maintain a separate visited set, but then it would have a different storage cost and might support different output requirements.

Bounds are checked before reading a neighboring cell. The starting coordinate is also validated explicitly. Coordinate mistakes often hide on a square center-cell example, so tests should include corners, edges, one-row grids, one-column grids, and a starting coordinate outside the grid.

Practice by starting at the lower-right one, then changing a zero region. Predict the changed count and entire resulting grid before running. Add diagonal directions in a separate variant and explain exactly which previously disconnected cells now belong to the same region.

Finally, state that this function mutates the supplied grid and returns the number of changed cells. A caller needing the original image must copy its storage first. The traversal uses work proportional to the visited region and queue storage for pending cells; correctness depends on the connectivity rule, boundary checks, and one-time marking policy together.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int[,] grid = { { 1, 1, 0 }, { 0, 1, 0 }, { 1, 0, 1 } };
Console.WriteLine(Fill(grid, 0, 0, 2));
for (int row = 0; row < grid.GetLength(0); row++) Console.WriteLine(string.Join(",", Enumerable.Range(0, grid.GetLength(1)).Select(column => grid[row, column])));
static int Fill(int[,] grid, int startRow, int startColumn, int replacement) {
    int rows = grid.GetLength(0), columns = grid.GetLength(1);
    if (startRow < 0 || startRow >= rows || startColumn < 0 || startColumn >= columns) throw new ArgumentOutOfRangeException();
    int original = grid[startRow, startColumn];
    if (original == replacement) return 0;
    var queue = new Queue<(int Row, int Column)>();
    queue.Enqueue((startRow, startColumn)); grid[startRow, startColumn] = replacement; int changed = 0;
    (int Row, int Column)[] directions = { (1, 0), (-1, 0), (0, 1), (0, -1) };
    while (queue.Count > 0) {
        var cell = queue.Dequeue(); changed++;
        foreach (var direction in directions) {
            int row = cell.Row + direction.Row, column = cell.Column + direction.Column;
            if (row >= 0 && row < rows && column >= 0 && column < columns && grid[row, column] == original) {
                grid[row, column] = replacement; queue.Enqueue((row, column));
            }
        }
    }
    return changed;
}

Expected output

3
2,2,0
0,2,0
1,0,1
Try this
Four-direction connectivity excludes diagonal cells; changing the value marks a cell as already queued.