Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions max-area-of-island/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
## 問題

[Max Area of Island - LeetCode](https://leetcode.com/problems/max-area-of-island/description/)

- 入力
- `grid`: バイナリ行列
- `1` は陸を表す
- 高さ `m`
- 1以上50以下
- 幅: `n`
- 1以上50以下
- 出力
- 島の最大面積

## 解法

### 1. 幅優先探索で島を探索し、面積の最大値を求める

- 以前やった島を数えるのとだいたい同じ
- grid をコピーする
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

訪問済みの島を管理するデータ構造について、選択肢の幅を検討できるとなお良いと思いました(前問で検討済みでしたらご放念ください)。

  • row, col -> bool のMap
  • [][]bool のスライス(grid と同じサイズ)

個人的には、処理中に WATER で塗りつぶしてしまうと、もとが島だったのかどうかの履歴が失われてしまい、デバッグにやや不便だったり、仕様変更や機能追加の際に手間がかかりそうに思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

別管理にする場合、状態を管理する構造体を定義し、そのメソッド内で参照する形にすると思います。
islandExplorer みたいな感じです

関数の引数として渡す形式にする場合、参照用の grid と管理用の何かを渡すので、それを嫌いました。

- 島を探索し、面積を出す
- 面積の最大値を更新する
- grid の探索が終わったら面積の最大値を返す
- 島がない場合は `0` を返す
- 時間計算量はO(mn)
- 空間計算量はO(mn)
- grid のコピー

## Step1

### 1.

```go
const (
WATER = 0
LAND = 1
)

func maxAreaOfIsland(grid [][]int) int {
// コピーした grid を扱う
grid = copyGrid(grid)

var maxArea int
for row := range grid {
for column := range grid[row] {
// 陸でなければスキップ
if grid[row][column] != LAND {
continue
}

startCell := Cell{
row: row,
column: column,
}
area := exploreIsland(startCell, grid)
maxArea = max(maxArea, area)
}
}

return maxArea
}

func copyGrid(grid [][]int) [][]int {
g := make([][]int, len(grid))
for row := range g {
g[row] = slices.Clone(grid[row])
}

return g
}

type Cell struct {
row int
column int
}

// 島を探索する
// 探索した島の大きさを返す
func exploreIsland(startCell Cell, grid [][]int) int {
rowSize := len(grid)
columnSize := len(grid[0])

offsets := []Cell{
// up
{row: -1, column: 0},
// right
{row: 0, column: 1},
// down
{row: 1, column: 0},
// left
{row: 0, column: -1},
}

// 最初の土地を登録する
// 重複して訪問しないよう水にする
area := 1
grid[startCell.row][startCell.column] = WATER

queue := []Cell{startCell}
for len(queue) > 0 {
cell := queue[0]
queue = queue[1:]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

スライスを上書きしても、背後のデータはそのままなんですね(コピーされない)。Pythonだとコピーされてしまうので羨ましいです。

https://go.dev/blog/slices-intro

Slicing does not copy the slice’s data. It creates a new slice value that points to the original array.


for _, offset := range offsets {
// 隣接セル
adjacent := Cell{
row: cell.row + offset.row,
column: cell.column + offset.column,
}

// 行が範囲外
if adjacent.row < 0 || rowSize <= adjacent.row {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

数直線をイメージしてその外側にあるからこの書き方なのでしょうか?
逆に!(0 <= row && row < rowSize)と書く方もいましたので紹介します。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。そのイメージです。

continue
}
// 列が範囲外
if adjacent.column < 0 || columnSize <= adjacent.column {
continue
}
// 陸でない
if grid[adjacent.row][adjacent.column] != LAND {
continue
}

// エリアとgridを更新する
area++
grid[adjacent.row][adjacent.column] = WATER

queue = append(queue, adjacent)
}
}

return area
}
```

## Step2

- グリッド内か判定する処理を関数にまとめる
- スタックを使うようにするとメモリ効率が多少改善される

```go
const (
WATER = 0
LAND = 1
)

func maxAreaOfIsland(grid [][]int) int {
grid = copyGrid(grid)

var maxArea int
for row := range grid {
for column := range grid[row] {
if grid[row][column] != LAND {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

下にあるisValidLandを使う、あるいは不要な分岐を省きたければisValidとisLandにわかれていれば、ここも関数で同じように書けるように見えます。

Copy link
Copy Markdown

@PafsCocotte PafsCocotte Feb 16, 2026

Choose a reason for hiding this comment

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

なお、今回のdfsの書き方は、陸であることを確認してから探検するという書き方ですが、海や境界外だったらすぐ帰るという書き方をすれば、関数最初にearly returnするだけで、exploreIslandを呼び出す際には海も境界も気にする必要がなくなるので、もう少し楽に書けると思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

そうですね、記述量は減ると思います。
私が読むとしたら、陸でないのに探索するのはなぜだろうと思ってしまうので、こんなふうに書いてしまいます。

continue
}

startCell := Cell{
row: row,
column: column,
}
area := exploreIsland(startCell, grid)
maxArea = max(maxArea, area)
}
}

return maxArea
}

func copyGrid(grid [][]int) [][]int {
g := make([][]int, len(grid))
for row := range g {
g[row] = slices.Clone(grid[row])
}

return g
}

type Cell struct {
row int
column int
}

func exploreIsland(startCell Cell, grid [][]int) int {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

私の理解が足りていなかったのですが、この関数は引数のstartCellが陸かつvalidでないといけないんですよね。
せっかく再利用もできるのですから、関数の責務をどこまで持つかという話ですが、陸を要求するならばassertや例外スローなどでそれを明示しておいた方が関数のユーザーにとって使いやすいです。

rowSize := len(grid)
columnSize := len(grid[0])
isValidLand := func(row, column int) bool {
Copy link
Copy Markdown

@PafsCocotte PafsCocotte Feb 16, 2026

Choose a reason for hiding this comment

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

ループの中で毎回これを生成するならば、外で関数定義したほうがいいと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

そうですね、外に出すなら構造体のメソッドとして定義します。

if row < 0 || rowSize <= row {
return false
}
if column < 0 || columnSize <= column {
return false
}

return grid[row][column] == LAND
}

offsets := []Cell{
{row: -1, column: 0},
{row: 0, column: 1},
{row: 1, column: 0},
{row: 0, column: -1},
}

area := 1
grid[startCell.row][startCell.column] = WATER

nextLands := []Cell{startCell}
for len(nextLands) > 0 {
land := nextLands[len(nextLands) - 1]
nextLands = nextLands[:len(nextLands) - 1]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ここに
grid[startCell.row][startCell.column] = WATER
を書けば、1個目の陸だけ特別扱いでこのfor文の前に沈めるとかせずに済んだように見えています。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

同じセルを2回以上スタックに積まないようにするために、入れる前に更新しています。
スタックから取り出した後に更新する場合、再度同じセルがスタックに積まれる可能性があると考えています。


スタックから取り出した後に更新する場合でも正しく大きさを計算できるか考えてみます。


for _, offset := range offsets {
adjacent := Cell{
row: land.row + offset.row,
column: land.column + offset.column,
}

if !isValidLand(adjacent.row, adjacent.column) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

valid は人によって想像する条件が異なりうるので、さらに具体的な命名にするとなお良いと思いました(今回の短さなら関数を読めばいいのですが、それすらスキップできる可能性もあります)。

  • grid内である
  • LAND である
  • 訪問済みである

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

LANDをWATERに変える方針にしたので、フィットする関数名に悩みました。
いまだに思いついてはいないので、改善するとしたら下記から選ぶと思います。

  • 関数ではなくそのまま記載する
  • コメントでフォローする

continue
}

area++
grid[adjacent.row][adjacent.column] = WATER

nextLands = append(nextLands, adjacent)
}
}

return area
}
```

### レビューを依頼する方のPR

- [695. Max Area of Island by mamo3gr · Pull Request #18 · mamo3gr/arai60](https://github.com/mamo3gr/arai60/pull/18)
- DFS
- [695. Max Area of Island by dxxsxsxkx · Pull Request #18 · dxxsxsxkx/leetcode](https://github.com/dxxsxsxkx/leetcode/pull/18)
- DFS
- [695. Max Area of Island by TakayaShirai · Pull Request #18 · TakayaShirai/leetcode_practice](https://github.com/TakayaShirai/leetcode_practice/pull/18)
- BFS
- [695_max_area_of_island by Hiroto-Iizuka · Pull Request #18 · Hiroto-Iizuka/coding_practice](https://github.com/Hiroto-Iizuka/coding_practice/pull/18)
- `frontier` という言葉がよく使われている
- 再帰版
- [695.max area of island by PafsCocotte · Pull Request #2 · PafsCocotte/leetcode](https://github.com/PafsCocotte/leetcode/pull/2)

## Step3

- 10分ギリギリ

```go
const (
WATER = 0
LAND = 1
)

func maxAreaOfIsland(grid [][]int) int {
grid = copyGrid(grid)

var maxArea int
for row := range grid {
for column := range grid[row] {
if grid[row][column] != LAND {
continue
}

area := exploreIsland(Cell{row: row, column: column}, grid)
maxArea = max(area, maxArea)
}
}

return maxArea
}

func copyGrid(grid [][]int) [][]int {
g := make([][]int, len(grid))
for row := range grid {
g[row] = slices.Clone(grid[row])
}
return g
}

type Cell struct {
row int
column int
}

func exploreIsland(start Cell, grid [][]int) int {
rowSize := len(grid)
columnSize := len(grid[0])
isValidLand := func(cell Cell) bool {
if cell.row < 0 || rowSize <= cell.row {
return false
}
if cell.column < 0 || columnSize <= cell.column {
return false
}

return grid[cell.row][cell.column] == LAND
}
offsets := []Cell{
{row: -1, column: 0},
{row: 0, column: 1},
{row: 1, column: 0},
{row: 0, column: -1},
}

area := 1
grid[start.row][start.column] = WATER

nextLands := []Cell{start}
for len(nextLands) > 0 {
land := nextLands[len(nextLands) - 1]
nextLands = nextLands[:len(nextLands) - 1]

for _, offset := range offsets {
adjacent := Cell{
row: land.row + offset.row,
column: land.column + offset.column,
}

if !isValidLand(adjacent) {
continue
}

area++
grid[adjacent.row][adjacent.column] = WATER

nextLands = append(nextLands, adjacent)
}
}

return area
}
```