Index Class Notes

Matrix
Data Structure

The Matrix data structure, in three parts. Part 1 covers matrix fundamentals, three core problem-solving approaches, and five coding problems. Part 2 covers BFS and DFS traversal on matrices with four additional problems. Part 3 is an extended practice set — the standard, high-frequency grid BFS/DFS problems that build directly on those patterns. Tips and interview tricks are highlighted throughout.


Part 1 — Fundamentals & Core Techniques (2 May)

I. Matrix Fundamentals

What is a Matrix?

A matrix is a two-dimensional array consisting of rows (horizontal) and columns (vertical). Each element is accessed by an index pair (row, column) — like an XY coordinate where X = row number, Y = column number.

For a matrix with 4 rows and 6 columns: indices range from (0,0) to (3,5). The element at row 2, column 3 is accessed as matrix[2][3].

Movement Rules

Key Concept

These two rules underpin every matrix problem:

  • Horizontal movement (left/right): Row stays the same, column changes. → matrix[row][col+1]
  • Vertical movement (up/down): Column stays the same, row changes. → matrix[row+1][col]

Worth understanding this deeply — index manipulation in 2D works exactly like 1D arrays, except you now track two coordinates instead of one.

Iteration & Time Complexity

Standard matrix traversal uses nested loops:

for (int i = 0; i < R; i++)      // rows
  for (int j = 0; j < C; j++)    // columns
    // operation on matrix[i][j]

Time complexity: O(R × C) — or O(M × N). If the matrix is square (N × N), this simplifies to O(N²).

II. Three Approaches for Matrix Problems

The Framework

Interview Strategy

Any matrix problem in interviews can be solved using one of three families of techniques:

  • BFS / DFS — Same crux as binary tree BFS/DFS, but applied to a 2D grid. Cell neighbors replace child nodes. Covered in Part 2.
  • Auxiliary Array — Create a helper array to precompute or cache values. Enables trading space for time.
  • Index Manipulation / Pointer Approach — Use pointers (like two-pointer in 1D) to navigate the matrix smartly. In 2D, this extends to four pointers (top, bottom, left, right).

Part 1 focused on techniques 2 and 3. BFS/DFS on matrices is covered in Part 2.

"Whenever you're stuck in an interview trying to reduce time complexity, always give a second thought: can I use extra space to bring it down? O(N²) time + O(1) space is worse than O(N) time + O(N) space."

On the auxiliary array trade-off

III. Tips, Tricks & Interview Advice

1. Space-Time Trade-off

Trick

If your brute-force is O(N³) or O(N²), ask yourself: can extra space reduce it? This is the single most common optimization interviewers probe for. An approach with O(N) time + O(N) space is strictly better than O(N²) time + O(1) space. Interviewers will often push you: "Can you do better?" — reaching for an auxiliary array is your first fallback.

2. BFS/DFS Obsession

Trick

Interviewers are "obsessed" with BFS and DFS on matrices. If you're given a matrix problem in an interview, chances are high they'll test your BFS/DFS skills. Make sure you've revised the BFS template (queue-based) and DFS (recursion-based) before any interview.

3. Don't Show You Know

Trick

Even if you've seen the exact problem before, pretend to think through it. If the interviewer senses you already know the answer, they'll skip to a harder problem you might not know. Take a moment, say "Let me think about this," then walk through the approach.

4. Interview Pacing

Trick

Standard expectation: solve 2 problems in 1 hour. Don't panic if one takes longer. But be aware: competition is increasing, and some companies now expect 3. Practice under timed conditions.

5. Negotiation Tip

Trick

When asked "How many offers do you have?" — always say you have 2 verbal offers and a third company where the final round went well. This signals demand. Never say you have zero offers, even if true. This is step 3 of the interview process: Land → Crack → Negotiate.

6. Video Interview Hygiene

Trick

Never have anyone sitting near you during a video interview. If the interviewer suspects someone is helping you — even from a whisper or a sudden change in your answer — they will reject you regardless of how well you answered. Don't take U-turns on answers mid-interview.

7. GitHub for Freshers

Trick

For college students: interviewers now check your GitHub commits. They want to verify you actually built what's on your resume (especially with AI making it easy to fake projects). Have a story ready for every resume point — what you did, why you did it, and how it works.

IV. Problem 1: Search in a Row-Column Sorted Matrix

Problem Statement

LC 240

Given an M × N matrix where every row and every column is sorted in increasing order, find the position of element X. If not present, return "Element not found."

Input: mat = {{10, 20, 30, 40}, {15, 25, 35, 45}, {27, 29, 37, 48}, {32, 33, 39, 50}}   x = 29
Output: Found at (2, 1)

The Core Insight

Key Idea

The trick is choosing the right starting position. If you start at (0,0) (top-left), both right and down give larger numbers — you can't decide which way to go. Same problem at (M-1, N-1) (bottom-right).

Solution: Start at the top-right corner (0, N-1) or bottom-left corner (M-1, 0).

  • From top-right: moving left gives smaller values, moving down gives larger values. Now you can always decide which direction to go based on comparison with the target.
  • From bottom-left: moving right gives larger values, moving up gives smaller values. Same logic, mirrored.

Walkthrough (x = 29)

Start at top-right: matrix[0][3] = 40

  • 40 > 29 → move left → matrix[0][2] = 30
  • 30 > 29 → move left → matrix[0][1] = 20
  • 20 < 29 → move down → matrix[1][1] = 25
  • 25 < 29 → move down → matrix[2][1] = 29 ✓ Found!

Code (Java)

public void search(int[][] mat, int m, int n, int x) {
    int i = 0, j = n - 1;   // start top-right
    while (i < m && j >= 0) {
        if (mat[i][j] == x) {
            print("Found at " + i + " " + j);
            return;
        }
        if (mat[i][j] > x) j--;             // move left (smaller)
        else i++;             // move down (larger)
    }
    print("Element not found");
}

Complexity

Time: O(max(M, N)) — you either exhaust a full row or a full column, never both nested. You eliminate an entire row or column with each comparison.

Space: O(1)

Why not O(M × N)? Because you never backtrack. Each step moves you strictly left or strictly down — you visit at most M + N cells.

V. Problem 2: Spiral Order Matrix Traversal

Problem Statement

LC 54

Given an M × N matrix, return all elements in spiral order.

Input:  [[1,  2,  3,  4],
         [5,  6,  7,  8],
         [9, 10, 11, 12]]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

The Four-Pointer Technique

Key Idea

Just as 1D arrays use two pointers, 2D spiral traversal uses four pointers acting as shrinking boundaries:

  • top = 0, bottom = M-1 (row boundaries)
  • left = 0, right = N-1 (column boundaries)

You traverse in four directions per cycle, shrinking the boundary after each pass:

  1. Left → Right along top row, then top++
  2. Top → Bottom along right column, then right--
  3. Right → Left along bottom row, then bottom--
  4. Bottom → Top along left column, then left++

Index Logic

Connects directly to the movement rules from Section I:

  • Going left→right: Row is fixed (top), column varies from left to right. → matrix[top][i]
  • Going top→bottom: Column is fixed (right), row varies from top to bottom. → matrix[i][right]
  • Going right→left: Row is fixed (bottom), column varies from right to left. → matrix[bottom][i] with i--
  • Going bottom→top: Column is fixed (left), row varies from bottom to top. → matrix[i][left] with i--

Boundary Check

Common Bug

Check boundary conditions immediately after updating each pointer, NOT in the outer while loop. If you only check in the outer while, the updated pointer might cause an index-out-of-bounds before the while condition is re-evaluated.

// After each pointer update:
top++;
if (left > right || top > bottom) break;
// ... next traversal ...
right--;
if (left > right || top > bottom) break;
// ... and so on

The outer loop is while(true) — you break out from inside when boundaries cross.

Code (Java)

public List<Integer> spiralOrder(int[][] matrix) {
    List<Integer> res = new ArrayList<>();
    if (matrix.length == 0) return res;

    int top = 0, bottom = matrix.length - 1;
    int left = 0, right = matrix[0].length - 1;

    while (true) {
        for (int i = left; i <= right; i++) res.add(matrix[top][i]);
        top++;
        if (left > right || top > bottom) break;

        for (int i = top; i <= bottom; i++) res.add(matrix[i][right]);
        right--;
        if (left > right || top > bottom) break;

        for (int i = right; i >= left; i--) res.add(matrix[bottom][i]);
        bottom--;
        if (left > right || top > bottom) break;

        for (int i = bottom; i >= top; i--) res.add(matrix[i][left]);
        left++;
        if (left > right || top > bottom) break;
    }
    return res;
}

Complexity

Time: O(M × N) — every element is visited exactly once.

Space: O(1) extra (excluding the output array).

VI. Problem 3: Maximal Square (Largest Square of 1s)

Problem Statement

LC 221

Given an M × N binary matrix filled with 0s and 1s, find the largest square containing only 1s and return its area.

The Auxiliary Array (DP) Technique

Key Idea

Create an auxiliary array of size (M+1) × (N+1). The extra row and column are initialized to 0. This is the bottom-up DP approach.

Why the extra row and column? Each cell in the DP table needs to compare with three neighbors: left, top, and diagonal-top-left. The padding row/column of zeros gives a safe base case so you never go out of bounds on the first row/column.

The DP Recurrence

Key Idea

For each cell where the original matrix value is 1:

dp[i][j] = min(dp[i][j-1],      // left
               dp[i-1][j-1],    // diagonal top-left
               dp[i-1][j])      // top
           + 1

Why minimum? If any one of the three neighbors is 0, you cannot form a larger square through this cell — the minimum captures this constraint. If all three are 1, min returns 1, and +1 gives you 2 (a 2×2 square). If all three are 2, you get a 3×3 square, and so on.

If the original value is 0: skip it entirely. A 0 can never be part of an all-1s square.

Connection to LCS

This is the same bottom-up DP pattern as Longest Common Subsequence: initialize base values, then fill the table using previously computed cells. Track the maximum value seen in the DP table. The answer is max_side × max_side (area = side²).

Code (Java)

public int maximalSquare(char[][] a) {
    if (a.length == 0) return 0;
    int m = a.length, n = a[0].length, result = 0;
    int[][] b = new int[m + 1][n + 1]; // DP array

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (a[i-1][j-1] == '1') {
                b[i][j] = Math.min(Math.min(b[i][j-1], b[i-1][j-1]), b[i-1][j]) + 1;
                result = Math.max(b[i][j], result);
            }
        }
    }
    return result * result;
}

Complexity

Time: O(M × N) — single pass through the matrix.

Space: O(M × N) — for the auxiliary DP array.

VII. Problem 4: Transpose of a Matrix

Problem Statement

LC 867

Given an N × N square matrix, return its transpose — flip it over the main diagonal so rows become columns and vice versa. matrix[i][j] swaps with matrix[j][i].

The main diagonal (where i == j) stays unchanged.

The Index Trick

Key Idea

The inner loop must start at j = i, NOT j = i + 1 or j = 0.

Why? Column 1 swaps with Row 1. Column 2 swaps with Row 2. If you use j = i + 1, you'd swap column 1 with row 2, producing incorrect results. If you use j = 0, you'd swap elements below the diagonal back (undoing the work).

For non-square matrices: in-place transpose isn't possible (dimensions change). You'd need a separate result matrix.

Code (Java)

public void transpose(int[][] matrix) {
    int n = matrix.length;
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
}

Complexity

Time: O(N²) — iterate through upper triangle of the matrix.

Space: O(1) — in-place swap with a temp variable.

VIII. Problem 5: Set Matrix Zeroes

Problem Statement

LC 73

Given an M × N integer matrix, if an element is 0, set its entire row and entire column to 0. You must do it in place (no auxiliary array).

Why Naive Fails

If you zero out rows/columns immediately upon finding a 0, you corrupt the matrix. The new zeros are indistinguishable from original zeros, causing a chain reaction that zeros out far more than intended.

The In-Place Indicator Trick

Key Idea

Use the first row and first column of the matrix itself as indicators. This is a two-pass approach:

Pass 1 — Mark indicators: Scan every cell. When you find matrix[i][j] == 0, set matrix[i][0] = 0 (mark that row) and matrix[0][j] = 0 (mark that column). Don't zero the actual row/column yet.

Pass 2 — Apply zeros: Iterate from (1,1) onward. If matrix[i][0] == 0 OR matrix[0][j] == 0, set matrix[i][j] = 0.

Handle first row/column separately: Since they're being used as indicators, you need a separate boolean to track whether column 0 originally contained a zero. Process them last.

Step-by-Step

  1. Use a flag col_no_0 to track if column 0 originally has any zero.
  2. Iterate all rows. If matrix[i][0] == 0, set col_no_0 = 1.
  3. For each cell starting from column 1: if matrix[i][j] == 0, mark matrix[0][j] = 0 and matrix[i][0] = 0.
  4. Iterate from (1,1): if either indicator is 0, zero the cell.
  5. If matrix[0][0] == 0, zero the entire first row.
  6. If col_no_0 == 1, zero the entire first column.

Complexity

Time: O(M × N) — two passes through the matrix.

Space: O(1) — only a single boolean variable. The matrix itself is used as storage.

Part 2 — BFS & DFS on Matrices (3 May)

IX. Direction Arrays: DX & DY

8-Directional Traversal

From any cell (x, y), you can move to 8 adjacent cells. The relative offsets are stored in two arrays:

int dx[8] = {-1, -1, -1, 0, 1, 1,  1,  0};
int dy[8] = {-1,  0,  1, 1, 1, 0, -1, -1};
(x, y) k0 (-1,-1) k1 (-1,0) k2 (-1,1) k3 (0,1) k4 (1,1) k5 (1,0) k6 (1,-1) k7 (0,-1) 4-dir subset k1 up · k3 right k5 down · k7 left

Fig. IX — the 8 neighbours of cell (x, y); each k walks clockwise from up-left, and (dx, dy) is its offset. The 4-dir set is just k1/k3/k5/k7.

Each pair (dx[k], dy[k]) walks the 8 neighbours clockwise: up-left, up, up-right, right, down-right, down, down-left, left.

Why this ordering

Any permutation of the 8 offsets is correct — BFS/DFS visit every neighbour regardless of order, so the sequence only matters for problems that care about tie-breaking. But this clockwise layout (top row left→right, down the right side, bottom row right→left, up the left side) is the standard one and the easiest to reproduce from memory under pressure. We use it consistently below.

The new cell coordinates become: nx = x + dx[k], ny = y + dy[k].

4-Directional Traversal

If you can only move north, south, east, west (no diagonals), the arrays shrink to size 4:

int dx[4] = {-1, 0, 1,  0};
int dy[4] = { 0, 1, 0, -1};
Interview Tip

Read the problem statement carefully — "8-directionally connected" means DX/DY of size 8; "horizontally or vertically" means size 4. The wrong array size will silently produce wrong answers.

X. Matrix as an Unweighted Graph

Why This Matters

Key Concept

A matrix is a type of unweighted graph. The cost to move from any cell to any adjacent cell is always 1 — there are no edge weights. This has a direct consequence for algorithm selection:

  • Shortest path in an unweighted graph/matrix → always use BFS.
  • Dijkstra's algorithm is for weighted graphs. Using Dijkstra on a matrix is overkill and a common interview mistake candidates make.

XI. BFS vs DFS: When to Use Which

At a Glance

BFS DFS
Breadth-First Search Depth-First Search
Explores level by level Explores one path as deep as possible
Uses a Queue Uses Recursion or a Stack
Best for shortest path in an unweighted graph Best for exploring everything
Time O(V + E) Time O(V + E)

For an R × C grid, V = R×C cells and each has up to 4 (or 8) edges, so both run in O(R×C).

Seeing It: Rings vs Depth

4 3 2 3 4 3 2 1 2 3 2 1 S 1 2 3 2 1 2 3 4 3 2 3 4 BFS — distance rings reached in increasing distance order 1 2 3 4 5 22 21 14 13 6 23 20 15 12 7 24 19 16 11 8 25 18 17 10 9 DFS — one deep path visit order dives before it spreads

Fig. XI — same 5×5 grid. BFS (left) reaches cells in increasing distance, so the target is found by the shortest route; DFS (right, arrows = visit tree) follows one branch to the end before backtracking.

BFS (Breadth-First Search)

Use BFS when you need the shortest path from a source to a destination in an unweighted graph or matrix. BFS visits nodes in increasing order of distance from the source:

Level 0 : source
Level 1 : 1 step away
Level 2 : 2 steps away
Level 3 : 3 steps away

Because levels expand outward one ring at a time, the first time BFS reaches the destination is guaranteed to be the shortest path — but only for unweighted graphs. Reach for BFS when the prompt says:

  • Shortest path · Minimum steps · Minimum moves
  • Nearest destination · Distance from source

Classic BFS problems: Shortest Path in Binary Matrix, Rotting Oranges, Word Ladder, 01 Matrix.

DFS (Depth-First Search)

Use DFS when you need to explore everything reachable from a starting point. DFS uses recursion (or an explicit stack): from each cell it goes as far as it can before backtracking. Reach for DFS when the prompt asks to:

  • Count islands · Find connected components · Flood fill
  • Explore all reachable cells · Detect cycles · Topological sort
  • Anything phrased as explore, count, traverse, visit all, connected
Why not DFS for shortest path?

DFS goes deep first, so it can march down a long branch before discovering a much shorter route elsewhere:

A
|
B
|
C
|
D

It can find the shortest path, but only after exploring many (or all) possible paths and comparing — far more work than BFS, which stops the instant it first touches the target.

Quick Decision Rule

Interview Strategy
Shortest Path / Minimum Steps
        ↓
      Use BFS

Count / Explore / Connected Components
        ↓
      Use DFS

Many problems (e.g. Number of Islands) work with either — DFS is usually shorter to write. But shortest path is always BFS.

Memory Trick

BFS
Queue
Level by Level
Shortest Path
Minimum Steps
DFS
Recursion / Stack
Deep First
Explore Everything
Count Components

Common Interview Problems

Problem BFS DFS
Number of Islands ✓ ✓
Flood Fill ✓ ✓
Rotting Oranges ✓ —
Shortest Path in Binary Matrix ✓ —
Word Ladder ✓ —
Clone Graph ✓ ✓
Connected Components ✓ ✓
Course Schedule (cycle detection) — ✓
Topological Sort ✓ (Kahn's) ✓

✓ = natural fit · — = technically possible but the wrong tool. Rotting Oranges and 01 Matrix are multi-source BFS (seed the queue with all sources at once); shortest-path problems want BFS's level guarantee, which DFS lacks.

XII. The BFS Template for Matrices

Template

int dx[] = {-1,-1,-1,0,1,1,1,0};   // 8-dir (swap in the 4-dir
int dy[] = {-1,0,1,1,1,0,-1,-1};   // arrays if diagonals aren't allowed)
int rows = grid.length, cols = grid[0].length;

Queue<int[]> queue = new LinkedList<>();
boolean[][] visited = new boolean[rows][cols];

queue.offer(new int[]{srcX, srcY});
visited[srcX][srcY] = true;
int distance = 0;

while (!queue.isEmpty()) {
    int size = queue.size();           // freeze this level's size

    for (int i = 0; i < size; i++) {
        int[] curr = queue.poll();
        int x = curr[0], y = curr[1];

        if (x == destX && y == destY) return distance;           // 0 when src == dest

        for (int k = 0; k < dx.length; k++) {
            int nx = x + dx[k];
            int ny = y + dy[k];
            if (nx < 0 || ny < 0 || nx>= rows || ny >= cols) continue;
            if (visited[nx][ny]) continue;
            visited[nx][ny] = true;    // mark BEFORE offering
            queue.offer(new int[]{nx, ny});
        }
    }

    distance++;                        // whole level done -> one more step
}
return -1;                             // destination unreachable

Template Breakdown

  1. Seed the queue with the source, mark it visited, set distance = 0.
  2. Process one level at a time: size = queue.size() freezes how many cells belong to the current level before you start polling.
  3. For each cell in the level: poll it and check if it's the destination — if so, return distance immediately.
  4. Explore neighbours: compute nx, ny from DX/DY, skip out-of-bounds or already-visited cells, otherwise mark visited and enqueue.
  5. Increment distance once, after the whole level is drained.
  6. Return -1 if the queue empties without reaching the target.

Why distance++ AFTER the level?

Each pass of the outer while processes exactly one BFS level — all cells the same number of steps from the source:

Level 0  (source)      distance = 0
   ↓
Level 1                distance = 1
   ↓
Level 2                distance = 2

Only after an entire level is drained have you actually moved one step further out, so the increment belongs at the end of the loop. Because the destination is checked before the increment, this correctly returns 0 when source == destination.

Cleaner than the alternative

Incrementing before processing forces an awkward return distance - 1 and an off-by-one you have to reason about every time. Increment-after needs no correction — prefer it.

Why Mark Visited BEFORE Enqueuing?

Mark a cell visited the moment you push it, not when you later pop it:

Correct

visited[nx][ny] = true;
queue.offer(...);

Wrong

queue.offer(...);
visited[nx][ny] = true;

If you defer the mark until pop, several neighbours in the same level can each enqueue the same cell before any of them is processed — inflating the queue and, in weighted-ish variants, corrupting distances.

Tracking Visited: Three Options

  1. 2D boolean array — boolean[][] visited. The default; simplest and the same pattern works for BFS and DFS.
  2. Mutate the grid in place — flip a cell's value (1→0 or 0→1) so it doubles as a blocker. Saves the extra array, but only when the problem allows changing the input.
  3. HashSet<String> or HashSet<Pair> — when coordinates are sparse or unbounded (e.g. an infinite board, as in Minimum Knight Moves) and no fixed-size matrix exists to index into.

XIII. The DFS Template for Matrices

Template

int dx[] = {-1, 0, 1,  0};   // 4-dir
int dy[] = { 0, 1, 0, -1};

void dfs(int x, int y, char[][] grid, boolean[][] vis) {
    int rows = grid.length, cols = grid[0].length;

    // Base condition -- every reason to stop lives here
    if (x < 0 || y < 0 || x>= rows || y >= cols) return;  // out of bounds
    if (vis[x][y])          return;                         // already seen
    if (grid[x][y] == '0')  return;                         // blocker cell

    vis[x][y] = true;

    for (int k = 0; k < 4; k++) dfs(x + dx[k], y + dy[k], grid, vis);               // no pre-check needed
}

This version is cleaner because every reason to stop lives in the base condition. The recursive call is a bare dfs(...) — the child validates itself the instant it runs.

Template Breakdown

  1. Base condition: return immediately if out of bounds, already visited, or a blocker cell. Read it as one question — "should I stop here?"
  2. Mark visited: vis[x][y] = true.
  3. Recurse into all neighbours using DX/DY — no guarding if around the call.
  4. Recursion unwinds naturally as every path eventually hits the base condition.
Do we need an if before the recursive call?

No. Since the base condition already rejects out-of-bounds, visited, and blocker cells, a guarding if (inBounds && !visited && open) dfs(...) is redundant. It's only a micro-optimization — it skips a function call that would return on its first line. Most interview solutions omit it because the bare call is simpler and less error-prone; add it only if call-stack overhead genuinely matters.

Blocker Cells

Depending on the problem, certain cell values act as blockers — you cannot traverse through them. In a binary matrix, 0 typically blocks (water), and 1 is traversable (land). Check grid[x][y] == blocker_value in the base condition.

XIV. Problem 6: Shortest Path in Binary Matrix

Problem Statement

LC 1091

Given an N × N binary matrix, return the length of the shortest clear path from (0,0) to (N-1, N-1). A clear path visits only cells with value 0 and moves 8-directionally. Return -1 if no path exists.

Approach

Key Idea

"Shortest path" + "unweighted" = BFS. Apply the BFS template with 8-directional DX/DY. Cells with value 1 are blockers. If the source cell grid[0][0] == 1, immediately return -1.

Instead of a separate visited array, mark visited cells by setting them to 1 in the grid itself (they become blockers). This saves O(N²) space.

Code (Java)

public int shortestPathBinaryMatrix(int[][] g) {
    int dx[] = {-1,-1,-1,0,1,1,1,0};
    int dy[] = {-1,0,1,1,1,0,-1,-1};
    int rows = g.length, cols = g[0].length;

    if (g[0][0] == 1) return -1;               // start is blocked

    Queue<int[]> queue = new LinkedList<>();
    queue.offer(new int[]{0, 0});
    g[0][0] = 1;                               // mark source visited (in-place)
    int distance = 1;                          // path LENGTH counts cells, so start at 1

    while (!queue.isEmpty()) {
        int size = queue.size();               // freeze this level's size

        for (int i = 0; i < size; i++) {
            int[] curr = queue.poll();
            int x = curr[0], y = curr[1];

            if (x == rows-1 && y == cols-1) return distance;               // reached the target cell

            for (int k = 0; k < dx.length; k++) {
                int nx = x + dx[k];
                int ny = y + dy[k];
                if (nx < 0 || ny < 0 || nx>= rows || ny >= cols) continue;
                if (g[nx][ny] == 1) continue;  // blocker or already visited
                g[nx][ny] = 1;                 // mark BEFORE offering
                queue.offer(new int[]{nx, ny});
            }
        }

        distance++;                            // whole level done -> one more cell out
    }
    return -1;                                 // destination unreachable
}

Complexity

Time: O(N²) — each cell visited at most once.

Space: O(N²) — for the queue in the worst case.

XV. Problem 7: Minimum Knight Moves

Problem Statement

LC 1197

On an infinite chessboard (coordinates from −∞ to +∞), a knight starts at (0,0). Return the minimum number of steps needed to reach square (x, y). Constraint: 0 ≤ |x| + |y| ≤ 300.

A knight moves in an L-shape: 2 squares in one cardinal direction + 1 square orthogonally. This gives 8 possible moves per position.

The Symmetry Trick

Key Idea

Knight moves are symmetrical across all four quadrants. If the knight can reach (-2, 1), it can equally reach (2, 1), (-2, -1), and (2, -1). Since the destination coordinates are given as |x| + |y| ≤ 300 (always non-negative after taking absolute values), you can:

  1. Take x = Math.abs(x), y = Math.abs(y) at the start.
  2. When computing new cell coordinates, take Math.abs(cx + dx[k]) instead of the raw value.

This confines the search to the first quadrant, dramatically reducing the search space.

Approach

"Minimum steps" = shortest path = BFS. The DX/DY arrays change to reflect knight movement:

int dx[] = {-2,-1,1,2, 2, 1,-1,-2};
int dy[] = { 1, 2,2,1,-1,-2,-2,-1};

Boundary: because we take Math.abs of each new coordinate, nx and ny are always ≥ 0, so the check simplifies to nx < 301 && ny < 301 (from the constraint). Visited array size: 301 × 301.

Edge case: no special-case needed — if the target is the origin, the template checks the destination before the first distance++ and returns 0 at level 0.

Code (Java)

public int minKnightMoves(int x, int y) {
    int dx[] = {-2,-1,1,2,2,1,-1,-2};
    int dy[] = {1,2,2,1,-1,-2,-2,-1};
    x = Math.abs(x);
    y = Math.abs(y);

    Queue<int[]> q = new LinkedList<>();
    boolean[][] vis = new boolean[301][301];
    q.offer(new int[]{0, 0});
    vis[0][0] = true;
    int distance = 0;                          // count MOVES, so start at 0

    while (!q.isEmpty()) {
        int size = q.size();

        for (int i = 0; i < size; i++) {
            int[] curr = q.poll();
            int cx = curr[0], cy = curr[1];

            if (cx == x && cy == y) return distance;               // 0 when the target is the origin

            for (int k = 0; k < dx.length; k++) {
                int nx = Math.abs(cx + dx[k]);
                int ny = Math.abs(cy + dy[k]);   // abs keeps nx, ny >= 0
                if (nx >= 301 || ny >= 301) continue;
                if (vis[nx][ny]) continue;
                vis[nx][ny] = true;            // mark BEFORE offering
                q.offer(new int[]{nx, ny});
            }
        }

        distance++;                            // one more knight move out
    }
    return -1;
}

Complexity

Time: O(M × N) where M, N are bounded by the constraint (at most 301 × 301).

Space: O(M × N) for the visited array.

XVI. Problem 8: Number of Islands

Problem Statement

LC 200

Given an M × N binary grid where '1' = land and '0' = water, return the number of islands. An island is a group of adjacent '1's connected horizontally or vertically (4-directional). All edges of the grid are surrounded by water.

Input: grid = [["1","1","0","0","0"],
               ["1","1","0","0","0"],
               ["0","0","1","0","0"],
               ["0","0","0","1","1"]]
Output: 3

The Connected Components Insight

Key Idea

Each island is a connected component of 1s. The answer equals the total number of DFS calls made from the main function. Each DFS call "sinks" an entire island by marking all its cells as visited, so no cell is counted twice.

  • Iterate through every cell in the matrix.
  • If grid[i][j] == '1' and not visited → call DFS, then answer++.
  • The DFS explores the entire connected island from that starting cell.

Code (Java)

int dx[] = {-1, 0, 1, 0};
int dy[] = { 0, 1, 0, -1};

public void dfs(int x, int y, char[][] grid, boolean[][] vis) {
    int rows = grid.length, cols = grid[0].length;

    if (x < 0 || y < 0 || x>= rows || y >= cols) return;  // out of bounds
    if (vis[x][y])         return;                         // already seen
    if (grid[x][y] == '0') return;                         // water = blocker

    vis[x][y] = true;

    for (int k = 0; k < 4; k++) dfs(x + dx[k], y + dy[k], grid, vis);              // no pre-check needed
}

public int numIslands(char[][] g) {
    int rows = g.length, cols = g[0].length;
    boolean[][] vis = new boolean[rows][cols];
    int ans = 0;
    for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) if (g[i][j] == '1' && !vis[i][j]) {
        dfs(i, j, g, vis);
        ans++;
    }
    return ans;
}

Complexity

Time: O(M × N) — each cell visited at most once across all DFS calls.

Space: O(M × N) — visited array + recursion stack in the worst case.

XVII. Problem 9: Max Area of Island

Problem Statement

LC 695

Given an M × N binary matrix, return the maximum area of an island. The area of an island is the number of cells with value 1. If there is no island, return 0.

The Twist: DFS Returns a Count

Key Idea

Same as Number of Islands, but the DFS function returns the count of 1s in the connected component instead of returning void. The main function tracks the maximum count across all DFS calls.

The DFS return value is: 1 + dfs(down) + dfs(right) + dfs(up) + dfs(left). The 1 counts the current cell; each recursive call returns the count from that direction.

Code (Java)

public int dfs(int x, int y, int[][] g, boolean[][] vis) {
    int rows = g.length, cols = g[0].length;

    if (x < 0 || y < 0 || x>= rows || y >= cols) return 0;  // out of bounds
    if (vis[x][y])    return 0;                             // already counted
    if (g[x][y] == 0) return 0;                             // water = blocker

    vis[x][y] = true;

    return 1                                    // this cell
    + dfs(x + 1, y, g, vis) + dfs(x, y + 1, g, vis) + dfs(x - 1, y, g, vis) + dfs(x, y - 1, g, vis);
}

public int maxAreaOfIsland(int[][] g) {
    int rows = g.length, cols = g[0].length;
    int ans = 0;
    boolean[][] vis = new boolean[rows][cols];
    for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) if (g[i][j] == 1 && !vis[i][j]) ans = Math.max(ans, dfs(i, j, g, vis));
    return ans;
}

Complexity

Time: O(M × N)

Space: O(M × N)

XVIII. Quick Reference: All Problems

Complexity Summary

Problem LC # Time Space Technique
Search in Sorted Matrix 240 O(M+N) O(1) Index Manipulation
Spiral Order 54 O(M×N) O(1) Four Pointers
Maximal Square 221 O(M×N) O(M×N) Auxiliary Array (DP)
Transpose 867 O(N²) O(1) Index Manipulation
Set Matrix Zeroes 73 O(M×N) O(1) In-place Indicators
Shortest Path Binary Matrix 1091 O(N²) O(N²) BFS (8-dir)
Minimum Knight Moves 1197 O(N²) O(N²) BFS + Symmetry
Number of Islands 200 O(M×N) O(M×N) DFS (4-dir)
Max Area of Island 695 O(M×N) O(M×N) DFS (4-dir)

Part 3 — Extended Practice Set

XIX. The Two Families Interviewers Reach For Most

Where to Focus

Priority
Key Idea

Once you can do plain Islands and plain shortest-path, two variations get tested most:

  • The border-flood trick — flood-fill from the edges first, then reason about what's left. Used in Closed Islands (1254), Enclaves (1020), Surrounded Regions (130), and Pacific Atlantic (417).
  • Multi-source BFS — seed the queue with all sources at once instead of one. Used in Rotting Oranges (994), 01 Matrix (542), Walls and Gates (286), and As Far from Land as Possible (1162).

Prioritize these two families. The other sections below round out the pattern space.

XX. Island / Connected-Components DFS

Siblings of Number of Islands

The closest siblings to Number of Islands — flood-fill a connected component, sometimes with a twist.

  • Flood Fill — the simplest version of the DFS you already wrote. LC 733
  • Max Area of Island — you have this, listed for completeness. LC 695
  • Number of Closed Islands — islands not touching the border (flood-fill from edges first). LC 1254
  • Number of Enclaves — count land cells that can't walk off the grid; same border-flood trick. LC 1020
  • Surrounded Regions — capture regions not connected to the border. LC 130
  • Pacific Atlantic Water Flow — two multi-source DFS from opposite borders, then intersect. LC 417
  • Number of Distinct Islands — same flood-fill, but hash each island's shape. LC 694
  • Making A Large Island — sink-and-relabel islands, then test each 0-flip. LC 827
  • Coloring A Border — DFS a component but only recolor its boundary. LC 1034

XXI. Shortest-Path BFS

Minimum Steps in an Unweighted Grid

"Minimum steps / shortest path in an unweighted grid" — the level-by-level BFS template from Part 2.

  • Rotting Oranges — multi-source BFS; seed the queue with all rotten oranges at once. LC 994
  • 01 Matrix — multi-source BFS from all 0s; classic distance-field problem. LC 542
  • Walls and Gates — multi-source BFS from every gate. LC 286
  • Shortest Path in a Grid with Obstacles Elimination — BFS where state is (row, col, obstacles_left). LC 1293
  • Shortest Bridge — DFS to mark one island, then BFS outward to reach the second. LC 934
  • As Far from Land as Possible — multi-source BFS from all land cells. LC 1162
  • Jump Game III — BFS/DFS reachability on an array treated as a graph. LC 1306

XXII. Weighted-Grid Shortest Path

When BFS Isn't Enough

The Boundary

Part 2 warned that Dijkstra is overkill for unweighted grids. These are the problems where BFS isn't enough and you graduate to a priority queue or 0-1 BFS — worth knowing so you recognize the boundary.

  • Path With Minimum Effort — Dijkstra / binary-search on a grid. LC 1631
  • Swim in Rising Water — Dijkstra-style / min-max path. LC 778
  • Path With Maximum Minimum Value — same min-max path family. LC 1102

XXIII. Grid DFS with Memoization (DFS + DP Crossover)

Caching Results

DFS where you cache results — bridges the flood-fill pattern with the DP work from Maximal Square.

  • Longest Increasing Path in a Matrix — DFS + memoization; a must-know. LC 329
  • Unique Paths III — backtracking DFS that must cover every cell. LC 980
  • Word Search — backtracking DFS through the grid. LC 79

XXIV. A Tight Practice Order

If the Goal is "Semi-Perfect" Efficiently

Do them in this sequence — each reuses the muscle from the previous:

  1. Flood Fill (733) → warm-up for the DFS template.
  2. Number of Islands (200) → Max Area (695) → Closed Islands (1254) / Enclaves (1020) → Surrounded Regions (130) → Pacific Atlantic (417). That chain drills the border-flood variation, the single most-tested twist.
  3. Rotting Oranges (994) → 01 Matrix (542) → Walls and Gates (286). This locks in multi-source BFS, the most common escalation from plain shortest-path.
  4. Shortest Bridge (934) → combines DFS + BFS in one problem, a favorite because it tests whether you know when to switch tools.
  5. Longest Increasing Path (329) → DFS + memo, the crossover into DP.
  6. Shortest Path with Obstacles Elimination (1293) → BFS with extended state, the hardest of the "standard" set.

XXV. What's Next

Next Class Preview

String Data Structure. The matrix module is now complete. Next weekend's class begins a new topic: strings.

Customize Report