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
- Use
dfsto explore each island, recording cells as offsets from the starting cell (origin). - For each newly discovered island, compare it against all previously stored unique islands:
- If sizes differ, they are different.
- Otherwise, compare cell by cell.
- If the island matches none of the stored islands, add it to the unique list.
- 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:
- Space complexity:
Where is the number of rows, and 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
- Use
dfsto explore each island, storing cells as relative coordinates from the starting cell. - Convert the set of coordinates to a frozenset (or equivalent hashable structure).
- Add the frozenset to a set of unique islands.
- 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:
- Space complexity:
Where is the number of rows, and 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
- Use
dfsto explore each island, recording the direction of each move (D, U, R, L). - After exploring all neighbors from a cell, append a backtrack marker (e.g., "0").
- Convert the path signature to a string and add it to a set of unique islands.
- 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:
- Space complexity:
Where is the number of rows, and 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.