Facebook Pixel

711. Number of Distinct Islands II πŸ”’

Problem Description

You have an m x n binary matrix grid where 1 represents land and 0 represents water. An island is formed by connecting adjacent lands horizontally or vertically (4-directionally connected). The entire grid is surrounded by water.

The key challenge is identifying distinct islands. Two islands are considered the same if:

  • They have the exact same shape, OR
  • One can be transformed into the other through:
    • Rotation: 90Β°, 180Β°, or 270Β° rotations
    • Reflection: flipping horizontally (left/right) or vertically (up/down)

Your task is to count how many distinct island shapes exist in the grid.

For example, if you have an L-shaped island and another island that looks like a rotated L, they count as the same shape (only 1 distinct island). Similarly, if one island is a mirror image of another, they're also considered the same.

The solution uses DFS to find all cells belonging to each island, then normalizes each island's shape by generating all 8 possible transformations (4 rotations Γ— 2 reflections). By comparing the canonical form of each island shape, duplicate shapes are eliminated using a set, giving us the count of truly distinct islands.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem involves a 2D grid where we need to explore connected components (islands). Each cell can be viewed as a node, and adjacent cells (4-directionally) form edges. Islands are essentially connected components in this implicit graph.

Is it a tree?

  • No: The grid structure is not a tree. It's a general graph where we have multiple disconnected components (islands), and the structure doesn't follow parent-child relationships like a tree.

Is the problem related to directed acyclic graphs?

  • No: The problem doesn't involve directed edges or acyclic properties. We're dealing with an undirected graph where connections between land cells are bidirectional.

Is the problem related to shortest paths?

  • No: We're not finding shortest paths between points. Instead, we need to identify and explore all cells belonging to each island to determine their shapes.

Does the problem involve connectivity?

  • Yes: The core of the problem is about connectivity - we need to find all connected components (islands) of 1s in the grid and identify their distinct shapes.

Does the problem have small constraints?

  • Yes: While not explicitly stated, grid problems typically have manageable constraints that allow for exploration algorithms. We need to explore each island completely to capture its shape.

DFS/backtracking?

  • Yes: DFS is perfect for exploring each island completely. Starting from any land cell, we recursively visit all connected land cells to capture the complete shape of the island.

Conclusion: The flowchart suggests using DFS (Depth-First Search) to explore connected components in the grid. DFS allows us to traverse each island completely, recording all cell positions to determine the island's shape for comparison with other islands after normalization.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we look at this problem, the first challenge is understanding what makes two islands "the same." If we only cared about exact matches, we could simply record each island's shape and compare them directly. But since islands can be rotated or reflected and still be considered identical, we need a way to handle all these transformations.

Think about it like puzzle pieces - if you have two L-shaped pieces, one might be rotated 90Β° from the other, but they're still the same piece. The key insight is that for any given shape, there are exactly 8 possible orientations when considering all rotations and reflections:

  • 4 rotations (0Β°, 90Β°, 180Β°, 270Β°)
  • Each rotation can also be reflected (flipped)

So our strategy becomes: for each island we find, generate all 8 possible transformations of its shape. But we still need a consistent way to compare shapes. This is where normalization comes in.

The normalization process works by:

  1. Taking all 8 transformations of an island's coordinates
  2. Sorting each transformation to create a consistent ordering
  3. Translating each shape so its "first" point is at origin (0, 0) - this makes the shape position-independent
  4. Choosing the lexicographically smallest transformation as the "canonical" representation

By using DFS to explore each island and collect its coordinates, then normalizing these coordinates to a canonical form, we can use a set to automatically handle duplicates. Two islands that are the same shape (considering rotations and reflections) will have the same canonical form and thus be counted only once.

The beauty of this approach is that we don't need to explicitly check if two islands match through various transformations - the normalization process handles all of that automatically by reducing every possible variant of a shape to a single, unique representation.

Learn more about Depth-First Search, Breadth-First Search and Union Find patterns.

Solution Approach

The implementation uses DFS combined with a normalization technique to identify distinct islands. Let's walk through the key components:

1. DFS Exploration (dfs function)

The DFS function explores an entire island starting from a given cell (i, j):

  • Records the current cell's coordinates in the shape list
  • Marks the cell as visited by setting grid[i][j] = 0
  • Recursively explores all 4 adjacent cells (up, down, left, right)
  • Builds a complete list of coordinates representing the island's shape
def dfs(i, j, shape):
    shape.append([i, j])
    grid[i][j] = 0
    for a, b in [[1, 0], [-1, 0], [0, 1], [0, -1]]:
        x, y = i + a, j + b
        if 0 <= x < m and 0 <= y < n and grid[x][y] == 1:
            dfs(x, y, shape)

2. Shape Normalization (normalize function)

This is the core logic that handles rotations and reflections. For each island shape, it generates all 8 possible transformations:

shapes[0].append([i, j])      # Original
shapes[1].append([i, -j])     # Reflect horizontally
shapes[2].append([-i, j])     # Reflect vertically
shapes[3].append([-i, -j])    # Rotate 180Β°
shapes[4].append([j, i])      # Rotate 90Β° counter-clockwise
shapes[5].append([j, -i])     # Rotate 90Β° + reflect
shapes[6].append([-j, i])     # Rotate 270Β° + reflect
shapes[7].append([-j, -i])    # Rotate 270Β°

After generating all transformations:

  • Each transformation is sorted to create consistent ordering
  • Each shape is translated so its first point becomes (0, 0):
    for i in range(len(e) - 1, -1, -1):
        e[i][0] -= e[0][0]
        e[i][1] -= e[0][1]
  • The lexicographically smallest transformation is chosen as the canonical form
  • The result is converted to a tuple (immutable and hashable) for set storage

3. Main Algorithm

The main loop:

  • Iterates through every cell in the grid
  • When a land cell (1) is found, initiates DFS to explore the entire island
  • Normalizes the island's shape to get its canonical representation
  • Adds the canonical form to a set (automatically handles duplicates)
  • Returns the size of the set, which gives the count of distinct islands
s = set()
for i in range(m):
    for j in range(n):
        if grid[i][j]:
            shape = []
            dfs(i, j, shape)
            s.add(normalize(shape))
return len(s)

The time complexity is O(m Γ— n Γ— k) where k is the average size of an island, as we need to normalize each island's shape. The space complexity is O(m Γ— n) for storing island shapes and the set of canonical forms.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate how the solution identifies distinct islands:

Grid:
1 1 0 0 0
1 0 0 0 0
0 0 0 1 1
0 0 0 0 1

Step 1: Find First Island Starting at (0,0), we encounter a '1'. DFS explores the island:

  • Visit (0,0), add to shape: [(0,0)]
  • Visit (0,1), add to shape: [(0,0), (0,1)]
  • Visit (1,0), add to shape: [(0,0), (0,1), (1,0)]
  • Mark all visited cells as 0

Island 1 coordinates: [(0,0), (0,1), (1,0)] - This is an L-shape.

Step 2: Normalize First Island Generate all 8 transformations of [(0,0), (0,1), (1,0)]:

  1. Original: [(0,0), (0,1), (1,0)]
  2. Reflect H: [(0,0), (0,-1), (1,0)]
  3. Reflect V: [(0,0), (0,1), (-1,0)]
  4. Rotate 180Β°: [(0,0), (0,-1), (-1,0)]
  5. Rotate 90Β° CCW: [(0,0), (1,0), (0,1)]
  6. Rotate 90Β° + reflect: [(0,0), (-1,0), (0,1)]
  7. Rotate 270Β° + reflect: [(0,0), (1,0), (0,-1)]
  8. Rotate 270Β°: [(0,0), (-1,0), (0,-1)]

After sorting each transformation and translating to origin:

  • Transform 1 sorted: [(0,0), (0,1), (1,0)] β†’ After translation: [(0,0), (0,1), (1,0)]
  • Transform 5 sorted: [(0,0), (0,1), (1,0)] β†’ After translation: [(0,0), (0,1), (1,0)]
  • (Other transforms produce different results)

Choose lexicographically smallest: ((0,0), (0,1), (1,0)) Add to set: {((0,0), (0,1), (1,0))}

Step 3: Find Second Island Starting at (2,3), we find another island:

  • Visit (2,3), add to shape: [(2,3)]
  • Visit (2,4), add to shape: [(2,3), (2,4)]
  • Visit (3,4), add to shape: [(2,3), (2,4), (3,4)]

Island 2 coordinates: [(2,3), (2,4), (3,4)] - This is also an L-shape, but rotated!

Step 4: Normalize Second Island Original coordinates: [(2,3), (2,4), (3,4)]

First, let's look at what happens with just the relative positions (ignoring the actual positions):

  • Relative to first point: [(0,0), (0,1), (1,1)]

Generate transformations:

  1. Original: [(0,0), (0,1), (1,1)]
  2. Rotate 90Β°: [(0,0), (1,0), (1,-1)]
  3. Rotate 180Β°: [(0,0), (0,-1), (-1,-1)]
  4. Rotate 270Β°: [(0,0), (-1,0), (-1,1)] ... and reflections

After normalization, one of the transformations (specifically a 270Β° rotation) produces:

  • Sorted and translated: [(0,0), (0,1), (1,0)]

This canonical form ((0,0), (0,1), (1,0)) already exists in our set!

Step 5: Final Result Set remains: {((0,0), (0,1), (1,0))} Count of distinct islands: 1

Even though we found two islands in different orientations, the normalization process correctly identified them as the same shape, giving us the correct count of 1 distinct island.

Solution Implementation

1class Solution:
2    def numDistinctIslands2(self, grid: List[List[int]]) -> int:
3        def dfs(row: int, col: int, island_cells: List[List[int]]) -> None:
4            """
5            Depth-first search to explore an island and collect all its cells.
6            Marks visited cells as 0 to avoid revisiting.
7            """
8            # Add current cell to the island shape
9            island_cells.append([row, col])
10            # Mark as visited
11            grid[row][col] = 0
12          
13            # Explore all 4 directions
14            directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]
15            for delta_row, delta_col in directions:
16                next_row = row + delta_row
17                next_col = col + delta_col
18                # Check bounds and if cell is part of island
19                if (0 <= next_row < rows and 
20                    0 <= next_col < cols and 
21                    grid[next_row][next_col] == 1):
22                    dfs(next_row, next_col, island_cells)
23      
24        def normalize(island_cells: List[List[int]]) -> Tuple[Tuple[int, ...]]:
25            """
26            Generate all 8 possible transformations (rotations and reflections)
27            of an island and return the lexicographically smallest one as canonical form.
28            """
29            # Generate all 8 transformations (4 rotations Γ— 2 reflections)
30            transformations = [[] for _ in range(8)]
31          
32            for row, col in island_cells:
33                # Original and reflections
34                transformations[0].append([row, col])      # Original
35                transformations[1].append([row, -col])     # Reflect over y-axis
36                transformations[2].append([-row, col])     # Reflect over x-axis
37                transformations[3].append([-row, -col])    # Rotate 180Β°
38                # 90Β° rotation and its reflections
39                transformations[4].append([col, row])      # Rotate 90Β° CCW
40                transformations[5].append([col, -row])     # Rotate 90Β° CCW + reflect
41                transformations[6].append([-col, row])     # Rotate 90Β° CW + reflect
42                transformations[7].append([-col, -row])    # Rotate 90Β° CW
43          
44            # Normalize each transformation by translating to origin
45            for transformation in transformations:
46                # Sort cells to ensure consistent ordering
47                transformation.sort()
48                # Translate so that first cell is at origin (0, 0)
49                base_row = transformation[0][0]
50                base_col = transformation[0][1]
51                for idx in range(len(transformation) - 1, -1, -1):
52                    transformation[idx][0] -= base_row
53                    transformation[idx][1] -= base_col
54          
55            # Sort all transformations and pick the lexicographically smallest
56            transformations.sort()
57          
58            # Convert to tuple for hashability
59            return tuple(tuple(cell) for cell in transformations[0])
60      
61        # Initialize grid dimensions
62        rows, cols = len(grid), len(grid[0])
63      
64        # Set to store unique island shapes
65        unique_islands = set()
66      
67        # Explore the entire grid
68        for row in range(rows):
69            for col in range(cols):
70                if grid[row][col] == 1:
71                    # Found an unvisited island
72                    island_cells = []
73                    dfs(row, col, island_cells)
74                    # Normalize and add to set of unique islands
75                    canonical_form = normalize(island_cells)
76                    unique_islands.add(canonical_form)
77      
78        return len(unique_islands)
79
1class Solution {
2    private int rows;
3    private int cols;
4    private int[][] grid;
5
6    /**
7     * Counts the number of distinct islands considering rotations and reflections.
8     * Two islands are considered the same if one can be rotated and/or reflected to match the other.
9     * 
10     * @param grid 2D binary grid where 1 represents land and 0 represents water
11     * @return number of distinct islands
12     */
13    public int numDistinctIslands2(int[][] grid) {
14        rows = grid.length;
15        cols = grid[0].length;
16        this.grid = grid;
17      
18        // Set to store unique normalized island shapes
19        Set<List<List<Integer>>> uniqueIslands = new HashSet<>();
20      
21        // Traverse the grid to find all islands
22        for (int i = 0; i < rows; ++i) {
23            for (int j = 0; j < cols; ++j) {
24                if (grid[i][j] == 1) {
25                    // Found an unvisited island cell
26                    List<Integer> islandCells = new ArrayList<>();
27                    dfs(i, j, islandCells);
28                    // Normalize the island shape and add to set
29                    uniqueIslands.add(normalize(islandCells));
30                }
31            }
32        }
33      
34        return uniqueIslands.size();
35    }
36
37    /**
38     * Normalizes an island shape by considering all 8 possible transformations
39     * (4 rotations Γ— 2 reflections) and returns the lexicographically smallest one.
40     * 
41     * @param islandCells list of encoded cell positions (row * cols + col)
42     * @return normalized representation of the island shape
43     */
44    private List<List<Integer>> normalize(List<Integer> islandCells) {
45        // Array to store all 8 possible transformations
46        List<int[]>[] transformations = new List[8];
47        for (int i = 0; i < 8; ++i) {
48            transformations[i] = new ArrayList<>();
49        }
50      
51        // Generate all 8 transformations for each cell
52        for (int encodedPosition : islandCells) {
53            int row = encodedPosition / cols;
54            int col = encodedPosition % cols;
55          
56            // 8 transformations: original, reflections, and rotations
57            transformations[0].add(new int[] {row, col});        // Original
58            transformations[1].add(new int[] {row, -col});       // Reflect horizontally
59            transformations[2].add(new int[] {-row, col});       // Reflect vertically
60            transformations[3].add(new int[] {-row, -col});      // Rotate 180Β°
61            transformations[4].add(new int[] {col, row});        // Transpose (rotate 90Β° + reflect)
62            transformations[5].add(new int[] {col, -row});       // Rotate 90Β° clockwise
63            transformations[6].add(new int[] {-col, row});       // Rotate 90Β° counter-clockwise
64            transformations[7].add(new int[] {-col, -row});      // Rotate 270Β° clockwise
65        }
66      
67        // Normalize each transformation
68        for (List<int[]> transformation : transformations) {
69            // Sort cells by row, then by column
70            transformation.sort((cell1, cell2) -> {
71                int row1 = cell1[0];
72                int col1 = cell1[1];
73                int row2 = cell2[0];
74                int col2 = cell2[1];
75                if (row1 == row2) {
76                    return col1 - col2;
77                }
78                return row1 - row2;
79            });
80          
81            // Translate the shape so that the top-left cell is at origin (0, 0)
82            int minRow = transformation.get(0)[0];
83            int minCol = transformation.get(0)[1];
84            for (int k = transformation.size() - 1; k >= 0; --k) {
85                int row = transformation.get(k)[0];
86                int col = transformation.get(k)[1];
87                transformation.set(k, new int[] {row - minRow, col - minCol});
88            }
89        }
90      
91        // Sort all transformations to find the lexicographically smallest one
92        Arrays.sort(transformations, (transformation1, transformation2) -> {
93            for (int k = 0; k < transformation1.size(); ++k) {
94                int row1 = transformation1.get(k)[0];
95                int col1 = transformation1.get(k)[1];
96                int row2 = transformation2.get(k)[0];
97                int col2 = transformation2.get(k)[1];
98                if (row1 != row2) {
99                    return row1 - row2;
100                }
101                if (col1 != col2) {
102                    return col1 - col2;
103                }
104            }
105            return 0;
106        });
107      
108        // Convert the smallest transformation to the required format
109        List<List<Integer>> normalizedShape = new ArrayList<>();
110        for (int[] cell : transformations[0]) {
111            normalizedShape.add(Arrays.asList(cell[0], cell[1]));
112        }
113      
114        return normalizedShape;
115    }
116
117    /**
118     * Performs depth-first search to explore all cells of an island.
119     * Marks visited cells as 0 to avoid revisiting.
120     * 
121     * @param row current row position
122     * @param col current column position
123     * @param islandCells list to store all cells of the current island
124     */
125    private void dfs(int row, int col, List<Integer> islandCells) {
126        // Add current cell to the island (encode position as single integer)
127        islandCells.add(row * cols + col);
128      
129        // Mark cell as visited
130        grid[row][col] = 0;
131      
132        // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
133        int[] directions = {-1, 0, 1, 0, -1};
134      
135        // Explore all 4 adjacent cells
136        for (int k = 0; k < 4; ++k) {
137            int nextRow = row + directions[k];
138            int nextCol = col + directions[k + 1];
139          
140            // Check if the adjacent cell is valid and is part of the island
141            if (nextRow >= 0 && nextRow < rows && 
142                nextCol >= 0 && nextCol < cols && 
143                grid[nextRow][nextCol] == 1) {
144                dfs(nextRow, nextCol, islandCells);
145            }
146        }
147    }
148}
149
1typedef pair<int, int> PII;
2
3class Solution {
4public:
5    int numDistinctIslands2(vector<vector<int>>& grid) {
6        set<vector<PII>> uniqueIslands;
7      
8        // Traverse the entire grid to find all islands
9        for (int row = 0; row < grid.size(); ++row) {
10            for (int col = 0; col < grid[0].size(); ++col) {
11                if (grid[row][col] == 1) {
12                    vector<PII> islandShape;
13                    // Perform DFS to collect all cells of the current island
14                    dfs(row, col, grid, islandShape);
15                    // Normalize the island shape to handle rotations and reflections
16                    uniqueIslands.insert(normalize(islandShape));
17                }
18            }
19        }
20      
21        return uniqueIslands.size();
22    }
23
24private:
25    // Normalize the island shape by considering all 8 possible transformations
26    // (4 rotations Γ— 2 reflections)
27    vector<PII> normalize(vector<PII>& shape) {
28        vector<vector<PII>> allTransformations(8);
29      
30        // Generate all 8 transformations of the island
31        for (const auto& point : shape) {
32            int x = point.first;
33            int y = point.second;
34          
35            // Original and reflections
36            allTransformations[0].push_back({x, y});      // Original
37            allTransformations[1].push_back({x, -y});     // Reflect along x-axis
38            allTransformations[2].push_back({-x, y});     // Reflect along y-axis
39            allTransformations[3].push_back({-x, -y});    // Rotate 180Β°
40          
41            // 90Β° rotation and its reflections
42            allTransformations[4].push_back({y, x});      // Rotate 90Β° counterclockwise
43            allTransformations[5].push_back({y, -x});     // Rotate 90Β° + reflect
44            allTransformations[6].push_back({-y, -x});    // Rotate 270Β° counterclockwise
45            allTransformations[7].push_back({-y, x});     // Rotate 270Β° + reflect
46        }
47      
48        // Normalize each transformation
49        for (auto& transformation : allTransformations) {
50            // Sort points to ensure consistent ordering
51            sort(transformation.begin(), transformation.end());
52          
53            // Translate all points so that the first point is at origin (0, 0)
54            int baseX = transformation[0].first;
55            int baseY = transformation[0].second;
56            for (int i = transformation.size() - 1; i >= 0; --i) {
57                transformation[i].first -= baseX;
58                transformation[i].second -= baseY;
59            }
60        }
61      
62        // Sort all transformations and return the lexicographically smallest one
63        sort(allTransformations.begin(), allTransformations.end());
64        return allTransformations[0];
65    }
66
67    // DFS to explore and mark all cells belonging to the current island
68    void dfs(int row, int col, vector<vector<int>>& grid, vector<PII>& shape) {
69        // Add current cell to the island shape
70        shape.push_back({row, col});
71      
72        // Mark current cell as visited by setting it to 0
73        grid[row][col] = 0;
74      
75        // Direction vectors for up, right, down, left
76        vector<int> directions = {-1, 0, 1, 0, -1};
77      
78        // Explore all 4 adjacent cells
79        for (int dir = 0; dir < 4; ++dir) {
80            int newRow = row + directions[dir];
81            int newCol = col + directions[dir + 1];
82          
83            // Check if the adjacent cell is valid and is part of the island
84            if (newRow >= 0 && newRow < grid.size() && 
85                newCol >= 0 && newCol < grid[0].size() && 
86                grid[newRow][newCol] == 1) {
87                dfs(newRow, newCol, grid, shape);
88            }
89        }
90    }
91};
92
1// Type definition for a coordinate pair
2type PII = [number, number];
3
4function numDistinctIslands2(grid: number[][]): number {
5    const uniqueIslands = new Set<string>();
6  
7    // Traverse the entire grid to find all islands
8    for (let row = 0; row < grid.length; row++) {
9        for (let col = 0; col < grid[0].length; col++) {
10            if (grid[row][col] === 1) {
11                const islandShape: PII[] = [];
12                // Perform DFS to collect all cells of the current island
13                dfs(row, col, grid, islandShape);
14                // Normalize the island shape to handle rotations and reflections
15                // Convert to string for Set comparison
16                uniqueIslands.add(JSON.stringify(normalize(islandShape)));
17            }
18        }
19    }
20  
21    return uniqueIslands.size;
22}
23
24// Normalize the island shape by considering all 8 possible transformations
25// (4 rotations Γ— 2 reflections)
26function normalize(shape: PII[]): PII[] {
27    const allTransformations: PII[][] = Array(8).fill(null).map(() => []);
28  
29    // Generate all 8 transformations of the island
30    for (const point of shape) {
31        const x = point[0];
32        const y = point[1];
33      
34        // Original and reflections
35        allTransformations[0].push([x, y]);        // Original
36        allTransformations[1].push([x, -y]);       // Reflect along x-axis
37        allTransformations[2].push([-x, y]);       // Reflect along y-axis
38        allTransformations[3].push([-x, -y]);      // Rotate 180Β°
39      
40        // 90Β° rotation and its reflections
41        allTransformations[4].push([y, x]);        // Rotate 90Β° counterclockwise
42        allTransformations[5].push([y, -x]);       // Rotate 90Β° + reflect
43        allTransformations[6].push([-y, -x]);      // Rotate 270Β° counterclockwise
44        allTransformations[7].push([-y, x]);       // Rotate 270Β° + reflect
45    }
46  
47    // Normalize each transformation
48    for (const transformation of allTransformations) {
49        // Sort points to ensure consistent ordering
50        transformation.sort((a, b) => {
51            if (a[0] !== b[0]) return a[0] - b[0];
52            return a[1] - b[1];
53        });
54      
55        // Translate all points so that the first point is at origin (0, 0)
56        const baseX = transformation[0][0];
57        const baseY = transformation[0][1];
58        for (let i = transformation.length - 1; i >= 0; i--) {
59            transformation[i][0] -= baseX;
60            transformation[i][1] -= baseY;
61        }
62    }
63  
64    // Sort all transformations and return the lexicographically smallest one
65    allTransformations.sort((a, b) => {
66        // Compare transformation arrays lexicographically
67        const minLength = Math.min(a.length, b.length);
68        for (let i = 0; i < minLength; i++) {
69            if (a[i][0] !== b[i][0]) return a[i][0] - b[i][0];
70            if (a[i][1] !== b[i][1]) return a[i][1] - b[i][1];
71        }
72        return a.length - b.length;
73    });
74  
75    return allTransformations[0];
76}
77
78// DFS to explore and mark all cells belonging to the current island
79function dfs(row: number, col: number, grid: number[][], shape: PII[]): void {
80    // Add current cell to the island shape
81    shape.push([row, col]);
82  
83    // Mark current cell as visited by setting it to 0
84    grid[row][col] = 0;
85  
86    // Direction vectors for up, right, down, left
87    const directions = [-1, 0, 1, 0, -1];
88  
89    // Explore all 4 adjacent cells
90    for (let dir = 0; dir < 4; dir++) {
91        const newRow = row + directions[dir];
92        const newCol = col + directions[dir + 1];
93      
94        // Check if the adjacent cell is valid and is part of the island
95        if (newRow >= 0 && newRow < grid.length && 
96            newCol >= 0 && newCol < grid[0].length && 
97            grid[newRow][newCol] === 1) {
98            dfs(newRow, newCol, grid, shape);
99        }
100    }
101}
102

Time and Space Complexity

Time Complexity: O(m * n * k * log(k)) where m and n are the dimensions of the grid, and k is the size of the largest island.

  • The outer loops iterate through all cells in the grid: O(m * n)
  • For each cell that starts an island, we perform DFS which visits all cells in that island: O(k) where k is the island size
  • The normalize function is called for each island:
    • Creating 8 transformations: O(8 * k) = O(k)
    • Sorting each of the 8 transformations: O(8 * k * log(k)) = O(k * log(k))
    • Normalizing coordinates by subtracting the first element: O(8 * k) = O(k)
    • Sorting the 8 transformations to find the canonical form: O(8 * log(8) * k) = O(k) (comparing lists of size k)
    • Converting to tuple: O(k)
    • Overall normalize: O(k * log(k))
  • In the worst case where we have O(m * n / k) islands, the total time is: O((m * n / k) * k * log(k)) = O(m * n * log(k))
  • However, when considering all operations including DFS traversal for all cells: O(m * n * k * log(k))

Space Complexity: O(m * n)

  • The shape list stores coordinates for one island at a time: O(k) where k is the island size
  • The shapes list in normalize stores 8 transformations: O(8 * k) = O(k)
  • The set s stores unique normalized island shapes. In the worst case, every cell could be a separate island: O(m * n)
  • The recursion stack depth for DFS can be at most O(m * n) in the worst case of a snake-like island
  • Overall space complexity: O(m * n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Modifying the Input Grid Without Restoration

The current solution modifies the input grid by setting visited cells to 0. This destructive approach can be problematic if:

  • The grid needs to be preserved for other operations
  • Multiple test cases run on the same grid instance
  • The problem explicitly states not to modify the input

Solution: Use a separate visited set to track explored cells instead of modifying the grid:

def dfs(row: int, col: int, island_cells: List[List[int]], visited: Set[Tuple[int, int]]) -> None:
    island_cells.append([row, col])
    visited.add((row, col))
  
    for delta_row, delta_col in [[1, 0], [-1, 0], [0, 1], [0, -1]]:
        next_row = row + delta_row
        next_col = col + delta_col
        if (0 <= next_row < rows and 
            0 <= next_col < cols and 
            grid[next_row][next_col] == 1 and
            (next_row, next_col) not in visited):
            dfs(next_row, next_col, island_cells, visited)

# In main loop:
visited = set()
for row in range(rows):
    for col in range(cols):
        if grid[row][col] == 1 and (row, col) not in visited:
            island_cells = []
            dfs(row, col, island_cells, visited)
            unique_islands.add(normalize(island_cells))

2. Incorrect Transformation Logic

A subtle but critical pitfall is incorrectly implementing the 8 transformations. The transformations must correctly represent all possible rotations and reflections:

Common mistakes:

  • Missing some transformations (e.g., only doing 4 instead of 8)
  • Incorrect rotation formulas (mixing up clockwise vs counter-clockwise)
  • Not handling both horizontal and vertical reflections

Solution: Verify transformations with a simple test case. For a point (x, y):

  • 90Β° CCW rotation: (-y, x)
  • 90Β° CW rotation: (y, -x)
  • 180Β° rotation: (-x, -y)
  • Horizontal reflection: (x, -y)
  • Vertical reflection: (-x, y)

3. Inefficient Normalization Process

The current implementation sorts transformations multiple times and creates many intermediate lists, which can be memory-intensive for large islands.

Solution: Optimize by finding the canonical form more efficiently:

def normalize(island_cells: List[List[int]]) -> Tuple[Tuple[int, ...]]:
    def get_canonical(cells):
        # Sort and translate to origin in one pass
        cells = sorted(cells)
        if not cells:
            return tuple()
        base_row, base_col = cells[0]
        return tuple((r - base_row, c - base_col) for r, c in cells)
  
    # Generate and compare transformations without storing all of them
    min_shape = None
    for transform_func in [
        lambda r, c: (r, c),      # Original
        lambda r, c: (r, -c),     # Reflect y
        lambda r, c: (-r, c),     # Reflect x
        lambda r, c: (-r, -c),    # Rotate 180
        lambda r, c: (c, r),      # Rotate 90 CCW
        lambda r, c: (c, -r),     # Rotate 90 CCW + reflect
        lambda r, c: (-c, r),     # Rotate 270 CCW
        lambda r, c: (-c, -r),    # Rotate 270
    ]:
        transformed = [transform_func(r, c) for r, c in island_cells]
        canonical = get_canonical(transformed)
        if min_shape is None or canonical < min_shape:
            min_shape = canonical
  
    return min_shape

4. Stack Overflow with Large Islands

For very large islands, the recursive DFS can cause stack overflow.

Solution: Use iterative DFS with an explicit stack:

def dfs_iterative(start_row: int, start_col: int) -> List[List[int]]:
    island_cells = []
    stack = [(start_row, start_col)]
    visited.add((start_row, start_col))
  
    while stack:
        row, col = stack.pop()
        island_cells.append([row, col])
      
        for delta_row, delta_col in [[1, 0], [-1, 0], [0, 1], [0, -1]]:
            next_row = row + delta_row
            next_col = col + delta_col
            if (0 <= next_row < rows and 
                0 <= next_col < cols and 
                grid[next_row][next_col] == 1 and
                (next_row, next_col) not in visited):
                visited.add((next_row, next_col))
                stack.append((next_row, next_col))
  
    return island_cells
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More