Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

200. Number of Islands

言語

Python

step1

DFS を用いる解法。

class Solution:
    def numIslands(self, grid: list[list[str]]) -> int:
        if not grid:
            return 0
        if not grid[0]:
            return 0
        num_rows = len(grid)
        num_columns = len(grid[0])
        num_islands = 0
        seen_lands = set()

        def is_land(row:int, column:int) -> bool:
            return (0 <= row < num_rows
                    and 0 <= column < num_columns
                    and grid[row][column] == "1")

        grid_diffs = ((1, 0), (-1, 0), (0, 1), (0, -1))
        def traverse_connected_lands(row, column):
            seen_lands.add((row, column))
            for dr, dc in grid_diffs:
                neighbor_row = row + dr
                neighbor_column = column + dc
                if not is_land(neighbor_row, neighbor_column):
                    continue
                if (neighbor_row, neighbor_column) in seen_lands:
                    continue
                traverse_connected_lands(neighbor_row, neighbor_column)
            return

        # find num_islands by multistart DFS
        for row in range(num_rows):
            for column in range(num_columns):
                # skip water
                if not is_land(row, column):
                    continue
                if (row, column) in seen_lands:
                    continue
                traverse_connected_lands(row, column)
                num_islands += 1
        return num_islands

DFSは、スタックの分だけの空間を使う上、Pythonでは再起呼び出し回数の制限がデフォルトでは1000と小さいので、パフォーマンス上はBFSを用いる方が望ましい。が、DFSの方がコードがシンプルになる場合が多い。

  • 時間計算量:O(n*m)
  • 空間計算量:O(n*m)

step2

class Solution:
    def numIslands(self, grid: list[list[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        num_rows = len(grid)
        num_columns = len(grid[0])
        num_islands = 0
        visited_lands = set()

        def is_land(row: int, column: int) -> bool:
            return (
                0 <= row < num_rows
                and 0 <= column < num_columns
                and grid[row][column] == "1"
            )

        # traverse connected lands by DFS
        directions = ((1, 0), (-1, 0),  (0, 1), (0, -1))
        def traverse_connected_lands(row, column):
            # if inplace: grid[row][column] = "0"
            visited_lands.add((row, column))
            for dr, dc in directions:
                neighbor_row = row + dr
                neighbor_column = column + dc
                if (neighbor_row, neighbor_column) in visited_lands:
                    continue
                if not is_land(neighbor_row, neighbor_column):
                    continue
                traverse_connected_lands(neighbor_row, neighbor_column)

        for row in range(num_rows):
            for column in range(num_columns):
                if not is_land(row, column):
                    continue
                if (row, column) in visited_lands:
                    continue
                traverse_connected_lands(row, column)
                num_islands += 1
        return num_islands
  • gridをinplaceに書き換えてOKならば、seen_lands.add((row, column))grid[row][column] = "0" に変更することで、seen_landsの代わりに元のグリッドを利用でき、空間計算量をO(1)に削減できる。

step3

class Solution:
    def numIslands(self, grid: list[list[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        WATER = "0"
        LAND = "1"
        num_rows = len(grid)
        num_cols = len(grid[0])
        num_islands = 0
        directions = ((1, 0), (-1, 0), (0, 1), (0, -1))
        def traverse(row, col):
            for dr, dc in directions:
                neighbor_row = row + dr
                neighbor_col = col + dc
                if neighbor_row < 0 or neighbor_row >= num_rows:
                    continue
                if neighbor_col < 0 or neighbor_col >= num_cols:
                    continue
                if grid[neighbor_row][neighbor_col] == WATER:
                    continue
                grid[neighbor_row][neighbor_col] = WATER
                traverse(neighbor_row, neighbor_col)

        for row in range(num_rows):
            for col in range(num_cols):
                if grid[row][col] == LAND:
                    traverse(row, col)
                    num_islands += 1
        return num_islands

step4 (FB)

別解・模範解答

BFS を用いる解法

bfs.py

from collections import deque

class Solution:
    def numIslands(self, grid: list[list[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        num_rows = len(grid)
        num_columns = len(grid[0])
        num_islands = 0
        num_islands = 0
        visited_lands = set()

        def is_land(row: int, column: int) -> bool:
            return (
                0 <= row < num_rows
                and 0 <= column < num_columns
                and grid[row][column] == "1"
            )

        directions = ((1, 0), (-1, 0),  (0, 1), (0, -1))
        def traverse_connected_lands(initial_row, initial_column):
            frontiers = deque([])
            frontiers.appendleft((initial_row, initial_column))
            visited_lands.add((initial_row, initial_column))
            while frontiers:
                row, column = frontiers.pop()
                for dr, dc in directions:
                    neighbor_row = row + dr
                    neighbor_column = column + dc
                    if not is_land(neighbor_row, neighbor_column):
                        continue
                    if (neighbor_row, neighbor_column) in visited_lands:
                        continue

                    visited_lands.add((neighbor_row, neighbor_column))
                    frontiers.appendleft((neighbor_row, neighbor_column))

        for row in range(num_rows):
            for column in range(num_columns):
                if not is_land(row, column):
                    continue
                if (row, column) in visited_lands:
                    continue
                traverse_connected_lands(row, column)
                num_islands += 1
        return num_islands

Disjoint Set Union (Union-Find) を用いる解法

dsu.py

class Solution:
    def numIslands(self, grid: list[list[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        num_rows = len(grid)
        num_cols = len(grid[0])

        def is_land(row: int, column: int) -> bool:
            return (
                0 <= row < num_rows
                and 0 <= column < num_cols
                and grid[row][column] == "1"
            )

        parents: dict[tuple[int, int], tuple[int, int]] = {}
        ranks: dict[tuple[int, int], int] = {}

        for row in range(num_rows):
            for col in range(num_cols):
                if is_land(row, col):
                    parents[(row, col)] = (row, col)
                    ranks[(row, col)] = 0

        def find(node):
            root = node
            while root != parents[root]:
                root = parents[root]
            while node != root:
                parent = parents[node]
                parents[node] = root
                node = parent
            return root

        def unite(node1, node2):
            root1 = find(node1)
            root2 = find(node2)
            if ranks[root1] > ranks[root2]:
                parents[root2] = root1
            elif ranks[root2] > ranks[root1]:
                parents[root1] = root2
            else:
                parents[root2] = root1
                ranks[root1] += 1


        directions = ((1, 0), (-1, 0),  (0, 1), (0, -1))
        for row in range(num_rows):
            for col in range(num_cols):
                if not is_land(row, col):
                    continue
                for dr, dc in directions:
                    neighbor = (row + dr, col + dc)
                    if is_land(neighbor[0], neighbor[1]):
                        unite((row, col), neighbor)

        roots = {find(cell) for cell in parents}
        return len(roots)
  • 時間計算量:O(n*m*α(k)) (αはアッカーマン関数の逆関数、kはunion/findの呼び出し回数)
  • 空間計算量:O(n*m)
  • mydsu.py は DSU をクラスとして実装した例。num_disjoint_sets を逐次管理することで、最後にルートの集合を数える計算を省略できる。

想定されるフォローアップ質問

  • Q. もしこのグリッドが非常に巨大で、例えば数テラバイトあり、メモリに一度に収まらない場合はどうしますか?
    • A. グリッドが2行分ずつメモリに収まると仮定します。各行でDSUと島の数を逐一計算し、前の行と現在の行で接続されている島をマージしていきます。これにより、メモリ使用量を大幅に削減しつつ、正確な島の数を計算できます。

次に解く問題の予告

ranks[root1] += 1


directions = ((1, 0), (-1, 0), (0, 1), (0, -1))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

右と下だけ調べれば十分だと思いました。

directions = ((1, 0), (0, 1))

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。確かにその通りですね。

if (neighbor_row, neighbor_column) in seen_lands:
continue
traverse_connected_lands(neighbor_row, neighbor_column)
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この return は文法上意味がないため、削除してよいと思います。

for dr, dc in directions:
neighbor_row = row + dr
neighbor_col = col + dc
if neighbor_row < 0 or neighbor_row >= num_rows:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

数直線上に一直線に並べたほうが読み手にとって読みやすくなると思います。

if not (0 <= neighbor_row and neighbor_row < num_rows):

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かにわかりやすいと思いました! 取り入れさせていただきます 🙇

return num_islands
```

DFSは、スタックの分だけの空間を使う上、Pythonでは再起呼び出し回数の制限がデフォルトでは1000と小さいので、パフォーマンス上はBFSを用いる方が望ましい。が、DFSの方がコードがシンプルになる場合が多い。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DFSをiterativeに(つまり再帰関数を使わずwhileやforなどを使って)書くこともでき、そうすれば再帰関数に関わるデメリットは無くなります。

https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.deivkzaqvetb

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。
確かにそうですね、再帰関数によるDFS実装、と書くべきですね。

return num_islands
```

- gridをinplaceに書き換えてOKならば、`seen_lands.add((row, column))` を`grid[row][column] = "0"` に変更することで、`seen_lands`の代わりに元のグリッドを利用でき、空間計算量を`O(1)`に削減できる。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

しかし、副作用が生じるのと呼び出した人はびっくりすると思うので、通常は関数入力に対してinplaceに操作する必要はないと思います。

frontiers.appendleft((initial_row, initial_column))
visited_lands.add((initial_row, initial_column))
while frontiers:
row, column = frontiers.pop()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここを.popleft()にすればDFS (iterative)になります。

for column in range(num_columns):
if not is_land(row, column):
continue
if (row, column) in visited_lands:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

()いらないですね。

visited_lands.add((initial_row, initial_column))
while frontiers:
row, column = frontiers.pop()
for dr, dc in directions:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dr, dcは少し認知負荷があると思います。
diff_row, diff_columnくらいが良いと思います。
しかし、今回は範囲がとても狭いのでこれくらい短くても許容かもしれません。

Comment on lines +157 to +162
if not (0 <= neighbor_row < num_rows):
continue
if not (0 <= neighbor_col < num_cols):
continue
if grid[neighbor_row][neighbor_col] == WATER:
continue

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

問題文に、

You may assume all four edges of the grid are all surrounded by water.

とあり、neighbor_row, neighbor_colが範囲ないかのチェックも、WATERではないかのチェックと言えると思います。

なのでこれはまとめて関数化してしまうとわかりやすいと思います。下でやられていますね。

num_rows = len(grid)
num_cols = len(grid[0])
num_islands = 0
directions = ((1, 0), (0, 1))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BFSやDFSの場合近傍探索は上下左右が必要かと思います。例えばコの字型の島の場合左への遷移が必要です。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます、それはその通りでした。directions = ((1, 0), (0, 1)) として良いのはDSUですね。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants