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 dfs to 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.
classSolution:defnumDistinctIslands(self, grid: List[List[int]])->int:defcurrent_island_is_unique():for other_island in unique_islands:iflen(other_island)!=len(current_island):continuefor cell_1, cell_2 inzip(current_island, other_island):if cell_1 != cell_2:breakelse:returnFalsereturnTrue# Do a DFS to find all cells in the current island.defdfs(row, col):if row <0or col <0or row >=len(grid)or col >=len(grid[0]):returnif(row, col)in seen ornot 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 inrange(len(grid)):for col inrange(len(grid[0])):
current_island =[]
row_origin = row
col_origin = col
dfs(row, col)ifnot current_island ornot current_island_is_unique():continue
unique_islands.append(current_island)print(unique_islands)returnlen(unique_islands)
classSolution{privateList<List<int[]>> uniqueIslands =newArrayList<>();// All known unique islands.privateList<int[]> currentIsland =newArrayList<>();// Current Islandprivateint[][] grid;// Input gridprivateboolean[][] seen;// Cells that have been explored. publicintnumDistinctIslands(int[][] grid){this.grid = grid;this.seen =newboolean[grid.length][grid[0].length];for(int row =0; row < grid.length; row++){for(int col =0; col < grid[0].length; col++){dfs(row, col);if(currentIsland.isEmpty()){continue;}// Translate the island we just found to the top left.int minCol = grid[0].length -1;for(int i =0; i < currentIsland.size(); i++){
minCol =Math.min(minCol, currentIsland.get(i)[1]);}for(int[] cell : currentIsland){
cell[0]-= row;
cell[1]-= minCol;}// If this island is unique, add it to the list.if(currentIslandUnique()){
uniqueIslands.add(currentIsland);}
currentIsland =newArrayList<>();}}return uniqueIslands.size();}privatevoiddfs(int row,int col){if(row <0|| col <0|| row >= grid.length || col >= grid[0].length)return;if(seen[row][col]|| grid[row][col]==0)return;
seen[row][col]=true;
currentIsland.add(newint[]{row, col});dfs(row +1, col);dfs(row -1, col);dfs(row, col +1);dfs(row, col -1);}privatebooleancurrentIslandUnique(){for(List<int[]> otherIsland : uniqueIslands){if(currentIsland.size()!= otherIsland.size()){continue;}if(equalIslands(currentIsland, otherIsland)){returnfalse;}}returntrue;}privatebooleanequalIslands(List<int[]> island1,List<int[]> island2){for(int i =0; i < island1.size(); i++){if(island1.get(i)[0]!= island2.get(i)[0]|| island1.get(i)[1]!= island2.get(i)[1]){returnfalse;}}returntrue;}}
Time & Space Complexity
Time complexity: O(M2⋅N2)
Space complexity: O(N⋅M)
Where M is the number of rows, and N 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 dfs to explore each island, storing cells as relative coordinates from the starting cell.
Convert the set of coordinates to a frozenset (or equivalent hashable structure).
classSolution:defnumDistinctIslands(self, grid: List[List[int]])->int:# Do a DFS to find all cells in the current island.defdfs(row, col):if row <0or col <0or row >=len(grid)or col >=len(grid[0]):returnif(row, col)in seen ornot 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 inrange(len(grid)):for col inrange(len(grid[0])):
current_island =set()
row_origin = row
col_origin = col
dfs(row, col)if current_island:
unique_islands.add(frozenset(current_island))returnlen(unique_islands)
classSolution{privateint[][] grid;privateboolean[][] seen;privateSet<Pair<Integer,Integer>> currentIsland;privateint currRowOrigin;privateint currColOrigin;privatevoiddfs(int row,int col){if(row <0|| row >= grid.length || col <0|| col >= grid[0].length){return;}if(grid[row][col]==0|| seen[row][col]){return;}
seen[row][col]=true;
currentIsland.add(newPair<>(row - currRowOrigin, col - currColOrigin));dfs(row +1, col);dfs(row -1, col);dfs(row, col +1);dfs(row, col -1);}publicintnumDistinctIslands(int[][] grid){this.grid = grid;this.seen =newboolean[grid.length][grid[0].length];Set<Set<Pair<Integer,Integer>>> islands =newHashSet<>();for(int row =0; row < grid.length; row++){for(int col =0; col < grid[0].length; col++){this.currentIsland =newHashSet<>();this.currRowOrigin = row;this.currColOrigin = col;dfs(row, col);if(!currentIsland.isEmpty()){
islands.add(currentIsland);}}}return islands.size();}}
Time & Space Complexity
Time complexity: O(M⋅N)
Space complexity: O(M⋅N)
Where M is the number of rows, and N 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 dfs to 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.
classSolution:defnumDistinctIslands(self, grid: List[List[int]])->int:# Do a DFS to find all cells in the current island.defdfs(row, col, direction):if row <0or col <0or row >=len(grid)or col >=len(grid[0]):returnif(row, col)in seen ornot 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 inrange(len(grid)):for col inrange(len(grid[0])):
path_signature =[]
dfs(row, col,"0")if path_signature:
unique_islands.add(tuple(path_signature))returnlen(unique_islands)
classSolution{privateint[][] grid;privateboolean[][] visited;privateStringBuffer currentIsland;publicintnumDistinctIslands(int[][] grid){this.grid = grid;this.visited =newboolean[grid.length][grid[0].length];Set<String> islands =newHashSet<>();for(int row =0; row < grid.length; row++){for(int col =0; col < grid[0].length; col++){
currentIsland =newStringBuffer();dfs(row, col,'0');if(currentIsland.length()==0){continue;}
islands.add(currentIsland.toString());}}return islands.size();}privatevoiddfs(int row,int col,char dir){if(row <0|| col <0|| row >= grid.length || col >= grid[0].length){return;}if(visited[row][col]|| grid[row][col]==0){return;}
visited[row][col]=true;
currentIsland.append(dir);dfs(row +1, col,'D');dfs(row -1, col,'U');dfs(row, col +1,'R');dfs(row, col -1,'L');
currentIsland.append('0');}}
classSolution{private:
vector<vector<int>>* grid;
vector<vector<bool>> visited;
string currentIsland;voiddfs(int row,int col,char dir){if(row <0|| col <0|| row >= grid->size()|| col >=(*grid)[0].size()){return;}if(visited[row][col]||(*grid)[row][col]==0){return;}
visited[row][col]=true;
currentIsland += dir;dfs(row +1, col,'D');dfs(row -1, col,'U');dfs(row, col +1,'R');dfs(row, col -1,'L');
currentIsland +='0';}public:intnumDistinctIslands(vector<vector<int>>& grid){this->grid =&grid;
visited =vector<vector<bool>>(grid.size(),vector<bool>(grid[0].size(),false));
unordered_set<string> islands;for(int row =0; row < grid.size(); row++){for(int col =0; col < grid[0].size(); col++){
currentIsland ="";dfs(row, col,'0');if(currentIsland.empty()){continue;}
islands.insert(currentIsland);}}return islands.size();}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/numDistinctIslands(grid){this.grid = grid;this.visited = Array.from({length: grid.length },()=>Array(grid[0].length).fill(false));const islands =newSet();for(let row =0; row < grid.length; row++){for(let col =0; col < grid[0].length; col++){this.currentIsland =[];this.dfs(row, col,'0');if(this.currentIsland.length ===0){continue;}
islands.add(this.currentIsland.join(''));}}return islands.size;}dfs(row, col, dir){if(row <0|| col <0|| row >=this.grid.length || col >=this.grid[0].length){return;}if(this.visited[row][col]||this.grid[row][col]===0){return;}this.visited[row][col]=true;this.currentIsland.push(dir);this.dfs(row +1, col,'D');this.dfs(row -1, col,'U');this.dfs(row, col +1,'R');this.dfs(row, col -1,'L');this.currentIsland.push('0');}}
Where M is the number of rows, and N 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.