1905. Count Sub Islands - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Depth First Search (DFS) - Recursively exploring connected components in a grid
  • Breadth First Search (BFS) - Level-by-level traversal using a queue for grid exploration
  • 2D Grid Traversal - Navigating a matrix using four-directional movement (up, down, left, right)
  • Union-Find (Disjoint Set Union) - Grouping connected components and checking connectivity (for DSU solution)

Intuition

An island in grid2 is a sub-island if every land cell of that island is also land in grid1. We can use DFS to explore each island in grid2 and simultaneously check if all its cells correspond to land in grid1. If any cell in the island exists in grid2 but not in grid1, the entire island is disqualified. We must explore the complete island before deciding, so we continue the DFS even after finding a mismatch.

Algorithm

  1. Create a visited set to track explored cells.
  2. For each unvisited land cell in grid2, start a DFS.
  3. In the DFS:
    • Return true if out of bounds, on water, or already visited (base cases).
    • Mark the current cell as visited.
    • Check if grid1 has land at this position; if not, the result becomes false.
    • Recursively explore all four directions, combining results with AND.
    • Return whether this island is a valid sub-island.
  4. Count and return the number of valid sub-islands.
class Solution:
    def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int:
        ROWS, COLS = len(grid1), len(grid1[0])
        visit = set()

        def dfs(r, c):
            if (min(r, c) < 0 or r == ROWS or c == COLS or
                grid2[r][c] == 0 or (r, c) in visit):
                return True

            visit.add((r, c))
            res = grid1[r][c]

            res &= dfs(r - 1, c)
            res &= dfs(r + 1, c)
            res &= dfs(r, c - 1)
            res &= dfs(r, c + 1)
            return res

        count = 0
        for r in range(ROWS):
            for c in range(COLS):
                if grid2[r][c] and (r, c) not in visit:
                    count += dfs(r, c)
        return count

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


Intuition

Similar to DFS, we can use BFS to explore islands in grid2. Starting from any unvisited land cell, we use a queue to visit all connected land cells level by level. During the traversal, we check if each cell also exists as land in grid1. If any cell fails this check, the island is not a sub-island, but we continue exploring to mark all cells as visited.

Algorithm

  1. Create a visited matrix and a directions array for the four neighbors.
  2. For each unvisited land cell in grid2, start a BFS.
  3. In the BFS:
    • Initialize a queue with the starting cell and mark it visited.
    • Set result to true.
    • While the queue is not empty:
      • Dequeue a cell; if grid1 has water at this position, set result to false.
      • Add all unvisited land neighbors in grid2 to the queue and mark them visited.
    • Return whether this island is a valid sub-island.
  4. Count and return the number of valid sub-islands.
class Solution:
    def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int:
        ROWS, COLS = len(grid1), len(grid1[0])
        visit = [[False] * COLS for _ in range(ROWS)]
        directions = [1, 0, -1, 0, 1]

        def bfs(r, c):
            queue = deque([(r, c)])
            visit[r][c] = True
            res = True

            while queue:
                r, c = queue.popleft()
                if grid1[r][c] == 0:
                    res = False

                for i in range(4):
                    nr, nc = r + directions[i], c + directions[i + 1]
                    if 0 <= nr < ROWS and 0 <= nc < COLS and not visit[nr][nc] and grid2[nr][nc]:
                        visit[nr][nc] = True
                        queue.append((nr, nc))
            return res

        count = 0
        for r in range(ROWS):
            for c in range(COLS):
                if grid2[r][c] == 1 and not visit[r][c]:
                    if bfs(r, c):
                        count += 1
        return count

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


3. Disjoint Set Union

Intuition

We can use Union-Find to group connected land cells in grid2 into islands. The key insight is that we also create a special "invalid" node (indexed at N, where N is the total number of cells). Any land cell in grid2 that corresponds to water in grid1 gets unioned with this invalid node. After processing, the number of sub-islands equals the total land cells minus the number of union operations performed (since each union either merges two components or marks one as invalid).

Algorithm

  1. Initialize a DSU with N+1 nodes (N cells plus one invalid node).
  2. For each land cell in grid2:
    • Count it as a land cell.
    • Union it with its right neighbor if the neighbor is also land in grid2.
    • Union it with its bottom neighbor if the neighbor is also land in grid2.
    • If grid1 has water at this position, union this cell with the invalid node N.
  3. Track the number of successful unions (where two different components merge).
  4. Return land count minus union count, which gives the number of valid sub-islands.
class DSU:
    def __init__(self, n):
        self.Parent = list(range(n + 1))
        self.Size = [1] * (n + 1)

    def find(self, node):
        if self.Parent[node] != node:
            self.Parent[node] = self.find(self.Parent[node])
        return self.Parent[node]

    def union(self, u, v):
        pu = self.find(u)
        pv = self.find(v)
        if pu == pv:
            return False
        if self.Size[pu] < self.Size[pv]:
            pu, pv = pv, pu
        self.Size[pu] += self.Size[pv]
        self.Parent[pv] = pu
        return True

class Solution:
    def countSubIslands(self, grid1, grid2):
        ROWS, COLS = len(grid1), len(grid1[0])
        N = ROWS * COLS
        dsu = DSU(N)

        def getId(r, c):
            return r * COLS + c

        land = unions = 0
        for r in range(ROWS):
            for c in range(COLS):
                if not grid2[r][c]:
                    continue
                land += 1
                if r + 1 < ROWS and grid2[r + 1][c]:
                    unions += dsu.union(getId(r, c), getId(r + 1, c))
                if c + 1 < COLS and grid2[r][c + 1]:
                    unions += dsu.union(getId(r, c), getId(r, c + 1))
                if not grid1[r][c]:
                    unions += dsu.union(getId(r, c), N)

        return land - unions

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


Common Pitfalls

Returning Early When a Mismatch is Found

A critical mistake is returning false immediately when finding a cell that exists in grid2 but not in grid1. This leaves part of the island unexplored and unvisited, causing those cells to be counted as separate islands later.

# Wrong: Early return leaves island partially visited
if grid1[r][c] == 0:
    return False  # Other cells in this island won't be marked

# Correct: Continue exploring, track result with a variable
res = grid1[r][c] == 1
res &= dfs(r - 1, c)  # Must visit all cells

Using OR Instead of AND for Recursive Results

When combining results from the four directional DFS calls, using OR (|) instead of AND (&) will incorrectly mark an island as a sub-island if any single cell matches, rather than requiring all cells to match.

Checking the Wrong Grid for Land Cells

When deciding whether to explore a neighboring cell, the check must be against grid2 (where we're finding islands), not grid1. The grid1 check determines validity, but grid2 determines connectivity.