Skip to content

200. Number of Islands#17

Open
seal-azarashi wants to merge 16 commits intomainfrom
number-of-islands
Open

200. Number of Islands#17
seal-azarashi wants to merge 16 commits intomainfrom
number-of-islands

Conversation

@seal-azarashi
Copy link
Copy Markdown
Owner

class Solution {
public int numIslands(char[][] grid) {
int numberOfIslands = 0;
boolean[][] traversed = new boolean[grid.length][grid[0].length];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

step2 のように下の行と入れ替えて、
boolean[][] traversed = new boolean[m][n];
としたほうがシンプルになると思います。

int n = grid[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '0' || traversed[i][j] == true) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if (grid[i][j] == '0' || traversed[i][j]) { のほうがシンプルだと思います。

}

private void traverseAdjacentCells(char[][] grid, boolean[][] traversed, int m, int n, int i, int j) {
boolean isOutOfBounds = i < 0 || i >= m || j < 0 || j >= n;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

boolean isInside = 0 <= i && i < m && 0 <= j && j < n;
if (!isInside || grid[i][j] == '0' || traversed[i][j]) {
    return;
}

としたほうが読みやすく感じます。

不等号の式の左右の方向は、比較されるほうを左に持ってくる派と、数直線上に一直線上に並べる派がいるように思います。実際の現場においては、チームのやり方に合わせることをお勧めします。

また、一般に、変数には肯定的な意味合いを持たせ、式の中で ! を使って否定したほうが読みやすくなると思います。今回の場合とは違うのですが、否定的な意味合いの変数を ! を使って否定すると、二重否定による肯定になり、認知負荷が上がり、読みにくく感じます。

Copy link
Copy Markdown
Owner Author

@seal-azarashi seal-azarashi Aug 27, 2024

Choose a reason for hiding this comment

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

ありがとうございます。

肯定的な意味合いの単語で構成され、判定の際に否定演算子を付与した上記の例の方が見やすく感じました。

また、たしかに数直線上に一直線上に並べる方が読みやすく感じました。今回特に意識せずに書いたら比較される方を左にしていましたが、仰る通り実際の業務ではチームのやり方に合せられればと思います。

ちなみに、どちらの書き方にするかについて、これまでの仕事においてチームで決まってたことってありましたか?
今までこれについて決めていたチームを見たことがなかったので、外資の大きい企業ではそういうこともあるのかな、と興味本位で気になりました。もし心当たりあれば参考までに教えて頂けると嬉しいです。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

コードレビューでなんでそうしたのー、って聞かれたことはありますね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

チームで決まっていたことはなかったように思います。 G 社時代に、自分が数直線上に並べる書き方をしていたところ、コードレビューで小西さんから比べられる変数を左側に持ってきた方が読みやすいと言われ、自分は数直線上に並べたほうが読みやすいと主張し、許してもらったことがあります。

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.

お二人ともありがとうございます!なるほどです。
(nodchip さんは小西さんと一緒に働かれてたんですね)

return numberOfIslands;
}

private void traverseAdjacentCells(char[][] grid, boolean[][] traversed, int m, int n, int i, int j) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 <= m, n <= 300 のため、最大で 90,000 回再帰する可能性があります。これによりスタックオーバーフローを引き起こす可能性があります。この点については考慮されましたか?

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.

別コメントで対応: #17 (comment)

boolean[][] traversed = new boolean[grid.length][grid[0].length];
int m = grid.length;
int n = grid[0].length;
for (int i = 0; i < m; i++) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

個人的には二次元の座標を表す変数は (x, y)、(row, column)、(r, c) を使うことが多いです。ただ、最近は (i, j) も使われているようです。
https://x.com/nodchip/status/1809223275442315414
ただし、 (i, j) が横方向と縦方向のどちらを表しているかについては注意が必要です。

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.

なるほど、個人的に意外な結果でした。


## 再帰関数の代わりにスタックを使う

(Leetcode の constraints ではそうならないとは書いてあるものの) 大きな 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.

Leetcode の constraints ではそうならないとは書いてある

これはどのように書かれていましたか?

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.

すみません、こちら勘違いでした。別のコメントでも指摘頂いている通り最大90,000スタックフレームが積まれるので、スタックオーバーフローになる可能性はあると思います。
確か oda さんが以前 Java の再帰呼び出しの限界について資料を貼ってくれていたので、そちら確認の上、後ほど訂正します。

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.

以下 oda さんの投稿からの引用です。Leetcode の constraints には "1 <= m, n <= 300" とあり、別のコメントでも指摘して頂いている通り 90,000 回再帰する可能性がありますので、スタックオーバーフローが起こる可能性があることは constraints からも明らかです。

Java は環境依存だけれども、1M くらいなので1万も行けばいいほうだよな、という感覚はあります。

from: https://discord.com/channels/1084280443945353267/1235829049511903273/1236256946403807323

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.

誤りのあった文面を訂正しました: #17

```

- "This class is likely to be faster than Stack when used as a stack" とある ArrayDeque を Stack クラスの代わりに使ってみた
- nodachip さんも言及されてた: https://github.com/goto-untrapped/Arai60/pull/38#discussion_r1683649014
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: nodchip

Copy link
Copy Markdown
Owner Author

@seal-azarashi seal-azarashi Aug 27, 2024

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

@seal-azarashi seal-azarashi Aug 27, 2024

Choose a reason for hiding this comment

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

修正済✍️: ea98eb2

- しかしメモリ消費量は1~2割ほど削減される
- 再帰関数を使った場合と比べおよそ2倍の処理時間がかかっている
- スタック的な役割のオブジェクトの利用よりも、スタックフレームの利用の方がよっぽど最適化されているということなのだろうか?
- 他に致命的な要因が無いかと record の代わりに整数型配列を使ったりもしたが、それほど大きな影響はなかった
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ArrayDeque に挿入するオブジェクトのヒープメモリの確保に時間がかかっている可能性があります。

一般に new は重い処理ですので、自然に避けられるのであれば避けたほうが無難だと思います。

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.

ありがとうございます。現在セルの数だけ new するようになっているので、避けるように工夫してみます。

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.

ArrayDeque の宣言を numIslands で一度だけ実施し、走査の度に実行される traverseAdjacentLands に渡すように修正したところ、興味深いことにむしろ少し処理が遅くなりました (7ms -> 9ms)。
こちら修正後の実装です。

class Solution {
    private record Cell(int row, int column) {};

    public int numIslands(char[][] grid) {
        int islandCount = 0;
        int rowCount = grid.length;
        int columnCount = grid[0].length;
        boolean[][] traversedLands = new boolean[rowCount][columnCount];
        Deque<Cell> traversingCells = new ArrayDeque<>();
        for (int row = 0; row < rowCount; row++) {
            for (int column = 0; column < columnCount; column++) {
                if (grid[row][column] == '0' || traversedLands[row][column] == true) {
                    continue;
                }

                traverseAdjacentLands(grid, traversedLands, traversingCells, rowCount, columnCount, row, column);
                islandCount++;
            }
        }

        return islandCount;
    }

    private void traverseAdjacentLands(char[][] grid, boolean[][] traversedLands, Deque<Cell> traversingCells, int rowCount, int columnCount, int row, int column) {
        traversingCells.add(new Cell(row, column));
        while (!traversingCells.isEmpty()) {
            Cell cell = traversingCells.removeFirst();
            boolean isOutOfBounds = !(cell.row >= 0 && cell.row < rowCount && cell.column >= 0 && cell.column < columnCount);
            if (isOutOfBounds || grid[cell.row][cell.column] == '0' || traversedLands[cell.row][cell.column] == true) {
                continue;
            }

            traversedLands[cell.row][cell.column] = true;

            traversingCells.add(new Cell(cell.row + 1, cell.column));
            traversingCells.add(new Cell(cell.row - 1, cell.column));
            traversingCells.add(new Cell(cell.row, cell.column + 1));
            traversingCells.add(new Cell(cell.row, cell.column - 1));
        }
    }
}

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.

JVM が実行していた何らかの最適化がされなくなり、その影響と修正の結果生じる関数への値渡し (厳密に言うと参照値の値渡しでしょうか) のオーバーヘッドが合わさって実行時間がよりかかるようになってしまったのかな、と推測しています。
もし他に原因になってそうなものがあればご教示頂けますと嬉しいです。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ArrayDeque の宣言を numIslands で一度だけ実施し、走査の度に実行される traverseAdjacentLands に渡すように修正したところ、興味深いことにむしろ少し処理が遅くなりました (7ms -> 9ms)。

他の方がおっしゃっていたのですが、 7ms -> 9ms というのが LeetCode 上での実行時間のことであれば、計測誤差が大きいため、あまり気にしないほうが良いと思います。ただ、桁が変わってくる場合には、根本的な部分の処理不可が変わった可能性もあるため、気にしたほうが良いかもしれません。

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.

はい、 Leetcode 上の実行時間のことです。そうですね、仕様上それなりの計測誤差が発生し得るのはしょうがないので、桁が変わるぐらいの変化で無い限りは基本的に気にしないようにしようと思います。

@seal-azarashi seal-azarashi requested a review from nodchip August 28, 2024 23:17
return;
}

grid[row][column] = '0';
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'がマジックナンバー化してしまってるので名前をつけてあげたほうが良いと思います

あと、訪問済みとするなら訪問済みを表現するための他の値を使ったほうがいいかと
grid[row][column] = '0';が海に塗り替える操作なのか、訪問済みとする操作なのか読者が意図を読み取れないので

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.

ありがとうございます。修正して Step 4 の解答に反映しました。

int islandCount = 0;
int rowCount = grid.length;
int columnCount = grid[0].length;
boolean[][] traversedLands = new boolean[rowCount][columnCount];
Copy link
Copy Markdown

@Yoshiki-Iwasa Yoshiki-Iwasa Aug 29, 2024

Choose a reason for hiding this comment

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

型と名前が合ってないと感じました

traversedLandsという変数名に格納されるデータは何かしらLandを表現するものであるべきかなと
この変数は訪問済みかどうかを表す真偽値を格納するのでisVisitedとかで十分なのではないでしょうか

追記
https://github.com/seal-azarashi/leetcode/pull/17/files#diff-5a321eb303c17e1955dbf9cf8de8318afdd9ff3496f0d55b36327caa0fb1682eR59

この問題も疑問形で始まる変数名なら大丈夫かなと

Copy link
Copy Markdown

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.

確かにそうですね。修正して Step 4 の解答に反映しました。


grid[row][column] = '0';
if (row - 1 >= 0 && grid[row - 1][column] == '1') {
unionFindIslands.union(row * columnCount + column, (row - 1) * columnCount + 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.

row * columnCount + columnを使いまわされているので変数に入れてもいいかもですね。

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.

確かにそうですね。修正しました: 0bdbe7b

@Ryotaro25
Copy link
Copy Markdown

全体的に読みやすいと感じました。


private void traverseAdjacentLands(char[][] grid, boolean[][] traversedLands, int rowCount, int columnCount, int row, int column) {
Deque<Cell> traversingCells = new ArrayDeque<>();
traversingCells.add(new Cell(row, 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.

Deque, removeFirst() を使われているので、addLast() のほうがなじむ気がします。

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.

ありがとうございます!こちらレビューで指摘してたのに自分のコードには反映出来てませんでしたね😇 Step 4 の実装に反映させました。

@goto-untrapped
Copy link
Copy Markdown

Java の裏側の処理など、参考になりました🙏

}

private int find(int i) {
if (parent[i] != i) {
Copy link
Copy Markdown

@nittoco nittoco Sep 2, 2024

Choose a reason for hiding this comment

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

細かいですが、個人的にはparent[i] == iの方を先に書きたい気持ちになりました(特殊な処理を先に書きたいので)

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.

確かにそうした方がよりしっくりくるなとと思いました。ありがとうございます。

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.

修正済: 6e6dd5e

Comment on lines +293 to +304
if (row - 1 >= 0 && grid[row - 1][column] == '1') {
unionFindIslands.union(root, (row - 1) * columnCount + column);
}
if (row + 1 < rowCount && grid[row + 1][column] == '1') {
unionFindIslands.union(root, (row + 1) * columnCount + column);
}
if (column - 1 >= 0 && grid[row][column - 1] == '1') {
unionFindIslands.union(root, row * columnCount + column - 1);
}
if (column + 1 < columnCount && grid[row][column + 1] == '1') {
unionFindIslands.union(root, root + 1);
}
Copy link
Copy Markdown

@nittoco nittoco Sep 2, 2024

Choose a reason for hiding this comment

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

自分の脳内処理が間違ってたら申し訳ないのですが、これって4方向繋ぐ必要ありましたっけ(左と上だけでいいような)
また個人的には、変数名をcurrent_column(rootのやつ), up_column((row - 1) * columnCount + columnのやつ)などとつけた方がいいように思いました。
rootは多分UnionFindを意識して書いていると思うのですが、読んでいる側からすると既にそれはわかってて、知りたいのはどの位置のgridを繋げているか、だと思いました。

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方向だけで良いですね。
root の変数名についても確かにその通りだなと思ったので、 coordinate に修正しました。
f086619

seal_azarashi added 2 commits September 3, 2024 07:43
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.

8 participants