Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph traversal (DFS) - Exploring all cells belonging to an island in a 2D grid
  • Coordinate normalization - Translating island shapes relative to an origin for comparison
  • Hashing - Using sets and hashable data structures to store unique island signatures
  • Path encoding - Recording traversal directions to create unique signatures for island shapes

1. Brute Force

Intuition

Two islands are considered the same if one can be translated (shifted) to match the other. To identify distinct islands, we need a way to represent each island's shape independent of its position. We do this by normalizing each island: recording each cell's position relative to the island's origin (the first cell we encounter). Then we compare each new island against all previously found unique islands.

Algorithm

  1. Use dfs to explore each island, recording cells as offsets from the starting cell (origin).
  2. For each newly discovered island, compare it against all previously stored unique islands:
    • If sizes differ, they are different.
    • Otherwise, compare cell by cell.
  3. If the island matches none of the stored islands, add it to the unique list.
  4. Return the count of unique islands.
class Solution:
    def numDistinctIslands(self, grid: List[List[int]]) -> int:
        
        def current_island_is_unique():
            for other_island in unique_islands:
                if len(other_island) != len(current_island):
                    continue
                for cell_1, cell_2 in zip(current_island, other_island):
                    if cell_1 != cell_2:
                        break
                else:
                    return False
            return True
            
        # Do a DFS to find all cells in the current island.
        def dfs(row, col):
            if row < 0 or col < 0 or row >= len(grid) or col >= len(grid[0]):
                return
            if (row, col) in seen or not grid[row][col]:
                return
            seen.add((row, col))
            current_island.append((row - row_origin, col - col_origin))
            dfs(row + 1, col)
            dfs(row - 1, col)
            dfs(row, col + 1)
            dfs(row, col - 1)
        
        # Repeatedly start DFS's as long as there are islands remaining.
        seen = set()
        unique_islands = []
        for row in range(len(grid)):
            for col in range(len(grid[0])):
                current_island = []
                row_origin = row
                col_origin = col
                dfs(row, col)
                if not current_island or not current_island_is_unique():
                    continue
                unique_islands.append(current_island)
        print(unique_islands)
        return len(unique_islands)

Time & Space Complexity

  • Time complexity: O(M2N2)O(M^2 \cdot N^2)
  • Space complexity: O(NM)O(N \cdot M)

Where MM is the number of rows, and NN is the number of columns


2. Hash By Local Coordinates

Intuition

Instead of comparing islands one by one, we can use a hash set for O(1) lookup. Each island is represented as a set of relative coordinates (offsets from the origin cell). Since sets of tuples are hashable (using frozenset in Python), we can directly add each island's shape to a set of unique islands. Duplicate shapes will naturally be filtered out.

Algorithm

  1. Use dfs to explore each island, storing cells as relative coordinates from the starting cell.
  2. Convert the set of coordinates to a frozenset (or equivalent hashable structure).
  3. Add the frozenset to a set of unique islands.
  4. Return the size of the unique islands set.
class Solution:
    def numDistinctIslands(self, grid: List[List[int]]) -> int:
        # Do a DFS to find all cells in the current island.
        def dfs(row, col):
            if row < 0 or col < 0 or row >= len(grid) or col >= len(grid[0]):
                return
            if (row, col) in seen or not grid[row][col]:
                return
            seen.add((row, col))
            current_island.add((row - row_origin, col - col_origin))
            dfs(row + 1, col)
            dfs(row - 1, col)
            dfs(row, col + 1)
            dfs(row, col - 1)
        
        # Repeatedly start DFS's as long as there are islands remaining.
        seen = set()
        unique_islands = set()
        for row in range(len(grid)):
            for col in range(len(grid[0])):
                current_island = set()
                row_origin = row
                col_origin = col
                dfs(row, col)
                if current_island:
                    unique_islands.add(frozenset(current_island))
        
        return len(unique_islands)

Time & Space Complexity

  • Time complexity: O(MN)O(M \cdot N)
  • Space complexity: O(MN)O(M \cdot N)

Where MM is the number of rows, and NN is the number of columns


3. Hash By Path Signature

Intuition

Another way to uniquely identify an island's shape is through its dfs traversal path. If we always explore directions in the same order (e.g., Down, Up, Right, Left), two identical shapes will produce the same sequence of moves. We record each direction taken during dfs, and importantly, we also record when we backtrack. This backtrack marker is crucial because without it, different shapes could produce the same direction sequence.

Algorithm

  1. Use dfs to explore each island, recording the direction of each move (D, U, R, L).
  2. After exploring all neighbors from a cell, append a backtrack marker (e.g., "0").
  3. Convert the path signature to a string and add it to a set of unique islands.
  4. Return the size of the unique islands set.
class Solution:
    def numDistinctIslands(self, grid: List[List[int]]) -> int:
        # Do a DFS to find all cells in the current island.
        def dfs(row, col, direction):
            if row < 0 or col < 0 or row >= len(grid) or col >= len(grid[0]):
                return
            if (row, col) in seen or not grid[row][col]:
                return
            seen.add((row, col))
            path_signature.append(direction)
            dfs(row + 1, col, "D")
            dfs(row - 1, col, "U")
            dfs(row, col + 1, "R")
            dfs(row, col - 1, "L")
            path_signature.append("0")
        
        # Repeatedly start DFS's as long as there are islands remaining.
        seen = set()
        unique_islands = set()
        for row in range(len(grid)):
            for col in range(len(grid[0])):
                path_signature = []
                dfs(row, col, "0")
                if path_signature:
                    unique_islands.add(tuple(path_signature))
        
        return len(unique_islands)

Time & Space Complexity

  • Time complexity: O(MN)O(M \cdot N)
  • Space complexity: O(MN)O(M \cdot N)

Where MM is the number of rows, and NN is the number of columns


Common Pitfalls

Forgetting to Normalize Island Coordinates

When comparing islands, you must translate each island's cells relative to a consistent origin (typically the first cell discovered). Without normalization, two identical shapes at different positions in the grid will appear different, leading to overcounting distinct islands.

Missing Backtrack Markers in Path Signatures

When using path-based hashing, recording only the direction of each DFS move is insufficient. Different island shapes can produce identical direction sequences if backtracking is not recorded. Always append a marker (like "0") when returning from a recursive call to distinguish branching structures.

Using Mutable Data Structures as Hash Keys

In languages like Python, using a list or set directly as a dictionary key will cause errors. You must convert to an immutable type (like frozenset or tuple) before adding to a set of unique islands. Similarly, in Java, ensure your island representation implements proper hashCode() and equals() methods if using custom objects.