6 minute read

Problem Statement

problem

Brute Force - BFS [TLE]

class Solution:
    def largestIsland(self, grid: List[List[int]]) -> int:
        res = 0
        ROWS = len(grid)
        COLS = len(grid[0])

        def bfs(row, col):
            val = grid[row][col]
            grid[row][col] = 1
            visited = [[0 for c in range(COLS)] for r in range(ROWS)]
            queue = deque([[row, col]])
            ans = 0
            while queue:
                r, c = queue.popleft()
                if visited[r][c] == 1:
                    continue
                visited[r][c] = 1
                ans += 1
                for x, y in [(0,1),(-1,0),(0,-1),(1,0)]:
                    nr, nc = r + x, c + y
                    if 0 <= nr < ROWS and 0 <= nc < COLS and grid[nr][nc] == 1 and visited[nr][nc] == 0:
                        queue.append([nr, nc])
            grid[row][col] = val
            return ans

        for row in grid:
            if any(True for x in row if x == 0):
                break
        else:
            return ROWS * COLS

        for row in grid:
            if any(True for x in row if x == 1):
                break
        else:
            return 1

        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == 0:
                    res = max(res, bfs(r, c))
        return res

DFS [Accepted]

class Solution:
  def largestIsland(self, grid: List[List[int]]) -> int:
    def dfs(r, c, index):
      if not (0 <= r < len(grid) and 0 <= c < len(grid[0]) and grid[r][c] == 1):
        return 0
      grid[r][c] = index
      return 1 + dfs(r+1, c, index) + dfs(r-1, c, index) + dfs(r, c+1, index) + dfs(r, c-1, index)

    n = len(grid)
    index = 2
    area = defaultdict(int)

    for r in range(n):
      for c in range(n):
        if grid[r][c] == 1:
          area[index] = dfs(r, c, index)
          index += 1

    res = max(area.values() or [0])

    for r in range(n):
      for c in range(n):
        if grid[r][c] == 0:
          seen = {grid[nr][nc] for nr, nc in [(r-1, c), (r+1, c), (r, c-1), (r, c+1)] if 0 <= nr < n and 0 <= nc < n}
          res = max(res, 1 + sum(area[i] for i in seen))

    return res

Notes: The BFS approach is slower than the DFS approach in this problem due to the following reasons:

Repeated Initialization: In the BFS approach, the visited matrix is re-initialized for each cell with value 0. This adds significant overhead, especially for larger grids.

Queue Operations: BFS uses a queue to manage the nodes to be visited. Queue operations (enqueue and dequeue) have additional overhead compared to the recursive stack used in DFS.

Multiple BFS Calls: The BFS approach calls the bfs function for each cell with value 0, leading to multiple traversals of the grid. In contrast, the DFS approach marks all connected components in a single pass and then only considers cells with value 0 once.

Grid Restoration: The BFS approach restores the grid value after each BFS call, adding extra operations.

The DFS approach is more efficient because it marks all connected components in a single pass and uses a more straightforward recursive approach without the need for repeated initialization or queue operations.

Editorial

Approach 1: Using DFS

class Solution:
    def largestIsland(self, grid: List[List[int]]) -> int:
        island_sizes = {}
        island_id = 2

        # Step 1: Mark all islands and calculate their sizes
        for current_row in range(len(grid)):
            for current_column in range(len(grid[0])):
                if grid[current_row][current_column] == 1:
                    island_sizes[island_id] = self.explore_island(
                        grid, island_id, current_row, current_column
                    )
                    island_id += 1

        # If there are no islands, return 1
        if not island_sizes:
            return 1

        # If the entire grid is one island, return its size or size + 1
        if len(island_sizes) == 1:
            island_id -= 1
            return (
                island_sizes[island_id]
                if island_sizes[island_id] == len(grid) * len(grid[0])
                else island_sizes[island_id] + 1
            )

        max_island_size = 1

        # Step 2: Try converting every 0 to 1 and calculate the resulting island size
        for current_row in range(len(grid)):
            for current_column in range(len(grid[0])):
                if grid[current_row][current_column] == 0:
                    current_island_size = 1
                    neighboring_islands = set()

                    # Check down
                    if (
                        current_row + 1 < len(grid)
                        and grid[current_row + 1][current_column] > 1
                    ):
                        neighboring_islands.add(
                            grid[current_row + 1][current_column]
                        )

                    # Check up
                    if (
                        current_row - 1 >= 0
                        and grid[current_row - 1][current_column] > 1
                    ):
                        neighboring_islands.add(
                            grid[current_row - 1][current_column]
                        )

                    # Check right
                    if (
                        current_column + 1 < len(grid[0])
                        and grid[current_row][current_column + 1] > 1
                    ):
                        neighboring_islands.add(
                            grid[current_row][current_column + 1]
                        )

                    # Check left
                    if (
                        current_column - 1 >= 0
                        and grid[current_row][current_column - 1] > 1
                    ):
                        neighboring_islands.add(
                            grid[current_row][current_column - 1]
                        )

                    # Sum the sizes of all unique neighboring islands
                    for island_id in neighboring_islands:
                        current_island_size += island_sizes[island_id]
                    max_island_size = max(max_island_size, current_island_size)

        return max_island_size

    def explore_island(
        self,
        grid: List[List[int]],
        island_id: int,
        current_row: int,
        current_column: int,
    ) -> int:
        if (
            current_row < 0
            or current_row >= len(grid)
            or current_column < 0
            or current_column >= len(grid[0])
            or grid[current_row][current_column] != 1
        ):
            return 0

        grid[current_row][current_column] = island_id

        return (
            1
            + self.explore_island(
                grid, island_id, current_row + 1, current_column
            )
            + self.explore_island(
                grid, island_id, current_row - 1, current_column
            )
            + self.explore_island(
                grid, island_id, current_row, current_column + 1
            )
            + self.explore_island(
                grid, island_id, current_row, current_column - 1
            )
        )

Approach 2: Using Disjoint Set Union (DSU)

class DisjointSet:
    def __init__(self, n: int):
        self.parent = [i for i in range(n)]
        self.island_size = [1] * n

    # Function to find the root of a node with path compression
    def find_root(self, node: int) -> int:

        if self.parent[node] == node:
            return node

        self.parent[node] = self.find_root(self.parent[node])
        return self.parent[node]

    # Function to union two sets based on size
    def union_nodes(self, node_a: int, node_b: int):

        root_a = self.find_root(node_a)
        root_b = self.find_root(node_b)

        # Already in the same set
        if root_a == root_b:
            return

        # Union by size: Attach the smaller island to the larger one
        if self.island_size[root_a] < self.island_size[root_b]:
            # Attach root_a to root_b
            self.parent[root_a] = root_b
            # Update size of root_b's island
            self.island_size[root_b] += self.island_size[root_a]
        else:
            # Attach root_b to root_a
            self.parent[root_b] = root_a
            # Update size of root_a's island
            self.island_size[root_a] += self.island_size[root_b]


class Solution:
    def largestIsland(self, grid: list[list[int]]) -> int:
        rows = len(grid)
        columns = len(grid[0])

        # Initialize DSU for the entire grid
        ds = DisjointSet(rows * columns)

        # Direction vectors for traversing up, down, left, and right
        row_directions = [1, -1, 0, 0]
        column_directions = [0, 0, 1, -1]

        # Step 1: Union adjacent `1`s in the grid
        for current_row in range(rows):
            for current_column in range(columns):
                if grid[current_row][current_column] == 1:

                    # Flatten 2D index to 1D
                    current_node = (columns * current_row) + current_column

                    for direction in range(4):
                        neighbor_row = current_row + row_directions[direction]
                        neighbor_column = (
                            current_column + column_directions[direction]
                        )

                        # Check bounds and ensure the neighbor is also `1`
                        if (
                            0 <= neighbor_row < rows
                            and 0 <= neighbor_column < columns
                            and grid[neighbor_row][neighbor_column] == 1
                        ):
                            neighbor_node = (
                                columns * neighbor_row + neighbor_column
                            )

                            ds.union_nodes(current_node, neighbor_node)

        # Step 2: Calculate the maximum possible island size
        max_island_size = 0

        # Flag to check if there are any zeros in the grid
        has_zero = False

        # To store unique roots for a `0`'s neighbors
        unique_roots = set()

        for current_row in range(rows):
            for current_column in range(columns):
                if grid[current_row][current_column] == 0:
                    has_zero = True

                    # Start with the flipped `0`
                    current_island_size = 1

                    for direction in range(4):
                        neighbor_row = current_row + row_directions[direction]
                        neighbor_column = (
                            current_column + column_directions[direction]
                        )

                        # Check bounds and ensure the neighbor is `1`
                        if (
                            0 <= neighbor_row < rows
                            and 0 <= neighbor_column < columns
                            and grid[neighbor_row][neighbor_column] == 1
                        ):
                            neighbor_node = (
                                columns * neighbor_row + neighbor_column
                            )

                            root = ds.find_root(neighbor_node)
                            unique_roots.add(root)

                    # Sum up the sizes of unique neighboring islands
                    for root in unique_roots:
                        current_island_size += ds.island_size[root]

                    # Clear the set for the next `0`
                    unique_roots.clear()

                    # Update the result with the largest island size found
                    max_island_size = max(max_island_size, current_island_size)

        # If there are no zeros, the largest island is the entire grid
        if not has_zero:
            return rows * columns
        return max_island_size