-
Notifications
You must be signed in to change notification settings - Fork 0
695 max area of island #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| def outer(): | ||
| x = 1 | ||
| def inner(): | ||
| return x | ||
| print(inner()) | ||
| return | ||
|
|
||
|
|
||
| def outer2(): | ||
| def inner2(): | ||
| return x | ||
| x = 1 | ||
| print(inner2()) | ||
| return |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,367 @@ | ||
| # Step1 | ||
|
|
||
| ## アプローチ | ||
|
|
||
| * `200. number of islands`のように, `LAND`を`WATER`に変えていく. その時に消した個数を記録して, その最大値を求める. | ||
| * 追記: 結局, visitedを使って再帰関数を面積を返す関数にして設計した | ||
| * 元の配列を破壊しないようにしたい. | ||
| * ユースケース的には, 一番大きい島を見つけたいだけ(変更はしたくなさそう) | ||
| * `DFS`でやる場合, Pythonのデフォルトの再帰上限は`1000`だから`setrecursionlimit`で変更してあげる必要がある. | ||
| * leetcodeでは大丈夫 | ||
| * 再帰回数の見積もり. 行数を`m`, 列数を`n`とする. | ||
| * `LAND`となっているものは`WATER`に書き換えられるので, たかだかマスの数分しか処理は行われない. | ||
| * 再帰の回数は最大で`m * n` | ||
| * 空間計算量 | ||
| * 配列を非破壊にするためにコピーするので`m * n` | ||
| * それとは別に, 再帰呼び出しでも`m * n`使う可能性がある. | ||
| * 全体で O(m * n) | ||
| * 時間計算量 | ||
| * 全てのマスを見るのでO(m * n) | ||
| * `UnionFind`を使ってもできそう, 同じグループに属しているものの個数の最大値を考えればいい | ||
| * 元の配列はコピーしなくても破壊することはない | ||
| * 各マスをみるので m * n回のunionが最大で行われる. | ||
| * 一回のunionにかかる処理の最悪の場合を考える. | ||
| * Nをunionfindのサイズとして, 逆アッカーマン関数 O(α(N)) | ||
| * でも逆アッカーマン関数がどのくらいか知らない... | ||
| * path compressionだけでlogNになっているから, それよりは早いはず... | ||
| * でも一番最初の方は, 最悪の場合にはならない | ||
| * 各要素が自身がrootになっているので | ||
| * O(m * n * α(N))とするのは不適切?? | ||
| * もちろん Upper boundではあるが... | ||
|
|
||
|
|
||
| ## Code1-1 (DFS) | ||
|
|
||
| ```python | ||
| from typing import List | ||
| import copy | ||
|
|
||
| class Solution: | ||
| def maxAreaOfIsland(self, grid: List[List[int]]) -> int: | ||
|
|
||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
|
|
||
| WATER = 0 | ||
| LAND = 1 | ||
|
|
||
| def calc_area_size_from_unvisited(row, col, visited): | ||
| if row < 0 or num_rows <= row or col < 0 or num_cols <= col: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if not 0 <= row < num_rows or not 0 <= col < num_cols:など、数直線所に一直線上に並べたほうが、読み手にとって分かりやすくなると思います。 |
||
| return 0 | ||
| if grid[row][col] == WATER: | ||
| return 0 | ||
| visited[row][col] = True | ||
| dirs = [(1, 0), (0, 1), (-1, 0), (0, -1)] | ||
| areas = 1 | ||
| for dr, dc in dirs: | ||
| next_row = row + dr | ||
| next_col = col + dc | ||
| if next_row < 0 or num_rows <= next_row: | ||
| continue | ||
| if next_col < 0 or num_cols <= next_col: | ||
| continue | ||
| if grid[next_row][next_col] == WATER: | ||
| continue | ||
| if visited[next_row][next_col]: | ||
| continue | ||
| areas += calc_area_size_from_unvisited(next_row, next_col, visited) | ||
| return areas | ||
|
|
||
| visited = [[False for _ in range(num_cols)] for _ in range(num_rows)] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. visited [[False] * num_cols for _ in range(num_rows)]と書くと、少しシンプルになると思います。 |
||
|
|
||
| max_areas = 0 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 最大の面積は一つしかないため、単数形で max_area としたほうが違和感が少なくなると思います。 |
||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| if not visited[r][c]: | ||
| area = calc_area_size_from_unvisited(r, c, visited) | ||
| max_areas = max(area, max_areas) | ||
|
|
||
| return max_areas | ||
|
|
||
| ``` | ||
|
|
||
| ## Code1-2 (UnionFind) | ||
|
|
||
| ```python | ||
| from typing import List | ||
| from collections import defaultdict | ||
|
|
||
| class UnionFind: | ||
| def __init__(self, size): | ||
| self.parent = [i for i in range(size)] | ||
| self.rank = [0] * size | ||
|
|
||
| def find(self, idx): | ||
| if self.parent[idx] == idx: | ||
| return idx | ||
| parent_idx = self.find(self.parent[idx]) | ||
| self.parent[idx] = parent_idx | ||
| return parent_idx | ||
|
Comment on lines
+97
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'if self.parent[idx] != idx' の処理を先にすると return がまとめられそうです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. def find(self, idx):
parent_idx = self.find(self.parent[idx])
self.parent[idx] = parent_idx
return parent_idxこういうことですかね. 確かにif文は不要になりそうです. 今回は、読み手のことを考えた時に(自然言語で動作を考えた時に), 「
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. あ、これ無限ループになりますね. すみませんが、わからなかったので
これどういうコードを想定しているか教えていただいてもいいですか? [追記] こういうことですかね. def find(self, idx):
if self.parent[idx] != idx:
parent_idx = self.find(self.parent[idx])
self.parent[idx] = parent_idx
return parent_idxThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 以下のコードは、等号の場合に find は次のように書けます。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
そうでしたね... 気づいてなかったです
確かに, こう考えると |
||
|
|
||
| def union(self, idx1, idx2): | ||
| parent_idx1 = self.find(idx1) | ||
| parent_idx2 = self.find(idx2) | ||
| if parent_idx1 == parent_idx2: | ||
| return | ||
|
|
||
| if self.rank[parent_idx1] < self.rank[parent_idx2]: | ||
| self.parent[parent_idx1] = parent_idx2 | ||
| return | ||
| elif self.rank[parent_idx2] < self.rank[parent_idx1]: | ||
| self.parent[parent_idx2] = parent_idx1 | ||
| return | ||
| else: | ||
| self.parent[parent_idx2] = parent_idx1 | ||
| self.rank[parent_idx1] += 1 | ||
| return | ||
|
|
||
|
|
||
| class Solution: | ||
| def maxAreaOfIsland(self, grid: List[List[int]]) -> int: | ||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
|
|
||
| WATER = 0 | ||
| LAND = 1 | ||
|
|
||
| uf = UnionFind(num_rows * num_cols) | ||
| flatten_row_col = lambda row, col : row * num_cols + col | ||
|
|
||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| if r + 1 < num_rows and grid[r + 1][c] == LAND: | ||
| uf.union(flatten_row_col(r, c), flatten_row_col(r + 1, c)) | ||
| if c + 1 < num_cols and grid[r][c + 1] == LAND: | ||
| uf.union(flatten_row_col(r, c), flatten_row_col(r, c + 1)) | ||
|
|
||
| group_number_to_area = defaultdict(int) | ||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| group_number_to_area[uf.find(flatten_row_col(r, c))] += 1 | ||
|
|
||
| if not group_number_to_area: | ||
| return 0 | ||
|
|
||
| return max(group_number_to_area.values()) | ||
|
|
||
| ``` | ||
|
|
||
| # Step2 | ||
|
|
||
| * どちらのコードも変更なし | ||
|
|
||
| ## Code2-1 (DFS) | ||
|
|
||
| ```python | ||
| from typing import List | ||
| import copy | ||
|
|
||
| class Solution: | ||
| def maxAreaOfIsland(self, grid: List[List[int]]) -> int: | ||
|
|
||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
|
|
||
| WATER = 0 | ||
| LAND = 1 | ||
|
|
||
| def calc_area_size_from_unvisited(row, col, visited): | ||
| if row < 0 or num_rows <= row or col < 0 or num_cols <= col: | ||
| return 0 | ||
| if grid[row][col] == WATER: | ||
| return 0 | ||
| visited[row][col] = True | ||
| dirs = [(1, 0), (0, 1), (-1, 0), (0, -1)] | ||
| areas = 1 | ||
| for dr, dc in dirs: | ||
| next_row = row + dr | ||
| next_col = col + dc | ||
| if next_row < 0 or num_rows <= next_row: | ||
| continue | ||
| if next_col < 0 or num_cols <= next_col: | ||
| continue | ||
| if grid[next_row][next_col] == WATER: | ||
| continue | ||
| if visited[next_row][next_col]: | ||
| continue | ||
| areas += calc_area_size_from_unvisited(next_row, next_col, visited) | ||
| return areas | ||
|
|
||
| visited = [[False for _ in range(num_cols)] for _ in range(num_rows)] | ||
|
|
||
| max_areas = 0 | ||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| if not visited[r][c]: | ||
| area = calc_area_size_from_unvisited(r, c, visited) | ||
| max_areas = max(area, max_areas) | ||
|
|
||
| return max_areas | ||
|
|
||
|
|
||
| ``` | ||
|
|
||
| ## Code2-2 (UnionFind) | ||
|
|
||
| ```python | ||
| from typing import List | ||
| from collections import defaultdict | ||
|
|
||
| class UnionFind: | ||
| def __init__(self, size): | ||
| self.parent = [i for i in range(size)] | ||
| self.rank = [0] * size | ||
|
|
||
| def find(self, idx): | ||
| if self.parent[idx] == idx: | ||
| return idx | ||
| parent_idx = self.find(self.parent[idx]) | ||
| self.parent[idx] = parent_idx | ||
| return parent_idx | ||
|
|
||
| def union(self, idx1, idx2): | ||
| parent_idx1 = self.find(idx1) | ||
| parent_idx2 = self.find(idx2) | ||
| if parent_idx1 == parent_idx2: | ||
| return | ||
|
|
||
| if self.rank[parent_idx1] < self.rank[parent_idx2]: | ||
| self.parent[parent_idx1] = parent_idx2 | ||
| return | ||
| elif self.rank[parent_idx2] < self.rank[parent_idx1]: | ||
| self.parent[parent_idx2] = parent_idx1 | ||
| return | ||
| else: | ||
| self.parent[parent_idx2] = parent_idx1 | ||
| self.rank[parent_idx1] += 1 | ||
| return | ||
|
|
||
|
|
||
| class Solution: | ||
| def maxAreaOfIsland(self, grid: List[List[int]]) -> int: | ||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
|
|
||
| WATER = 0 | ||
| LAND = 1 | ||
|
|
||
| uf = UnionFind(num_rows * num_cols) | ||
| flatten_row_col = lambda row, col : row * num_cols + col | ||
|
|
||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| if r + 1 < num_rows and grid[r + 1][c] == LAND: | ||
| uf.union(flatten_row_col(r, c), flatten_row_col(r + 1, c)) | ||
| if c + 1 < num_cols and grid[r][c + 1] == LAND: | ||
| uf.union(flatten_row_col(r, c), flatten_row_col(r, c + 1)) | ||
|
|
||
| group_number_to_area = defaultdict(int) | ||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| group_number_to_area[uf.find(flatten_row_col(r, c))] += 1 | ||
|
|
||
| if not group_number_to_area: | ||
| return 0 | ||
|
|
||
| return max(group_number_to_area.values()) | ||
|
|
||
|
|
||
| ``` | ||
|
|
||
| ## Memo | ||
|
|
||
| * `setrecursionlimit`について | ||
| * https://github.com/olsen-blue/Arai60/pull/18#discussion_r1919805259 | ||
| * > まあ、setrecursionlimit できるような環境ならば設定すればいいわけですが、たとえば、ライブラリーを作っているとすると、これはグローバルに設定を変えることになるので他のところに影響が出る可能性がありますね。 | ||
|
|
||
| * `stack`による`DFS` | ||
| * `setrecursionlimit`を変更することに忌避感があるのであれば, stackを用いた`DFS `も検討できる | ||
| * https://github.com/aki235/Arai60/pull/18/files#diff-9fe83a5ebd0765dc3d775b7bbf3048d8f6ff02247c075977086bf46f21a077b4R98 | ||
|
|
||
| * 内部関数の仕様について気になったから調べた | ||
| * 内部関数はその内部関数の実行時に関数オブジェクトが作られる. | ||
| * 内部関数の定義後に定義した変数も関数オブジェクトに含まれるのでは? | ||
| * スコープでは, どちらも外部関数内の変数として扱われるはず. | ||
| * 以下の例はどちらも同じ動作をした | ||
|
|
||
| ```python | ||
| def outer(): | ||
| x = 1 | ||
| def inner(): | ||
| return x | ||
| print(inner()) | ||
| return | ||
|
|
||
|
|
||
| def outer2(): | ||
| def inner2(): | ||
| return x | ||
| x = 1 | ||
| print(inner2()) | ||
| return | ||
| ``` | ||
|
|
||
|
|
||
|
|
||
| ## Code2-3 (DFS with Stack) | ||
|
|
||
| ```python | ||
| from typing import List | ||
|
|
||
| class Solution: | ||
| def maxAreaOfIsland(self, grid: List[List[int]]) -> int: | ||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
| WATER = 0 | ||
| LAND = 1 | ||
| visited = [[False for _ in range(num_cols)] for _ in range(num_rows)] | ||
|
|
||
| def get_area_of_island(row, col): | ||
| assert not visited[row][col] | ||
| to_visit = [(row, col)] | ||
| area_size = 0 | ||
| while to_visit: | ||
| r, c = to_visit.pop() | ||
| if visited[r][c]: | ||
| continue | ||
| visited[r][c] = True | ||
| area_size += 1 | ||
| dirs = [(1, 0), (0, 1), (-1, 0), (0, -1)] | ||
| for dr, dc in dirs: | ||
| next_r = r + dr | ||
| next_c = c + dc | ||
| if next_r < 0 or num_rows <= next_r: | ||
| continue | ||
| if next_c < 0 or num_cols <= next_c: | ||
| continue | ||
| if grid[next_r][next_c] == WATER: | ||
| continue | ||
| if visited[next_r][next_c]: | ||
| continue | ||
| to_visit.append((next_r, next_c)) | ||
| return area_size | ||
|
|
||
| maximum_area = 0 | ||
| for r in range(num_rows): | ||
| for c in range(num_cols): | ||
| if grid[r][c] == WATER: | ||
| continue | ||
| if visited[r][c]: | ||
| continue | ||
| area = get_area_of_island(r, c) | ||
| maximum_area = max(area, maximum_area) | ||
| return maximum_area | ||
|
|
||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
個人的にはクラスインスタンスとして定義したいですが、好みの範囲だと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
すみません、クラスインスタンスを調べたのですがあまりわかなくて、以下のどちらかを意味していますか?
この二つは自分も検討しましたがそれぞれ理由があって、採用しませんでした.
これは考慮すべきほど大きな問題じゃない(誤差)なのでしょうか。
アクセスしたい時に
grid[row][col] == PLACE.WATER.valueのようになって.valueが挟まる分可読性が悪くなるかもしれないと考えたため不採用グローバルスコープに
WATERとLANDが存在することになるため, バイトコードでLOAD_GLOBALが呼ばれる分, 関数内に定義した場合に呼ばれるLOAD_FASTよりもオーバーヘッドが大きいと考えたため不採用.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
オーバーヘッドが大きいとのことですが、そのオーバーヘッドはどの程度 (例えば何 ms 程度) でしょうか?また、そのオーバーヘッドは、コードの読みやすさと比較して考慮すべきほどの大きさでしょうか?
なお、 Python で書いている時点で、 C/C++ などの高速な言語と比べると実行速度はけた違いに遅い場合が多いです。そのため、仮に LOAD_GLOBAL が LOAD_FAST より遅かったとしても、全体の実行時間に対する影響は相対的に小さいケースが多いように思います。
もしその程度の差でもパフォーマンスが問題になるのであれば、 Python 内で細かな最適化をするよりも、 C/C++/Rust/Java/C# など、より高速な言語で実装することを検討するほうが合理的な場面もあると思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/kitano-kazuki/leetcode/pull/10/changes#diff-0c860cd754249868513e4f9054206317fa33d0f548fc3896ac2b3e11822fd852R267
10^4 * log10^4 stepで, 0.004秒 = 7msでした
1stepあたりで考えると, 0.004 * 10^3 * 10^3 / 10^4 = 0.4 ns 程度ですかね.
コードの読みやすさが同じくらいならば考慮してもいいくらいだというのが自分の感覚です.
コードの読みやすさが著しく変わる(理解するのに+10secかかるくらい)ならば, 考慮しなくていい感覚です.
これはおっしゃる通りだと思っていて, その上でどのくらいのオーバヘッドから考慮すべきなのかの感覚が自分ではまだ自信がないです. ユースケースによると思いますが、 例えばリアルタイム性を重視するプログラムであれば, 同じコードの実行に0.1秒以上差があるなら,
相対的に小さいとは言えなくなってきそうな感覚があります.これはまさにその通りですね.