Skip to content
Open
Show file tree
Hide file tree
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
18 changes: 18 additions & 0 deletions 62_unique_paths_medium/1dp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
num_paths_in_row = [0] * n
num_paths_in_row[0] = 1
for _ in range(m):
for column in range(1, n):
num_paths_in_row[column] += num_paths_in_row[column - 1]
return num_paths_in_row[-1]


# @lc code=end
21 changes: 21 additions & 0 deletions 62_unique_paths_medium/2dp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
num_paths = [[0] * n for _ in range(m)]
for row in range(m):
for column in range(n):
if row == 0 or column == 0:
num_paths[row][column] = 1
continue
num_paths[row][column] = num_paths[row - 1][column] + num_paths[row][column - 1]

return num_paths[-1][-1]


# @lc code=end
117 changes: 117 additions & 0 deletions 62_unique_paths_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 問題へのリンク
[Unique Paths - LeetCode](https://leetcode.com/problems/unique-paths/description/)

# 言語
Python

# 問題の概要
m x n のグリッドが与えられる。左上のマスから右下のマスまで、右または下にのみ移動する場合のユニークな経路の総数を求める問題である。

# 自分の解法

この問題は、最終的に `(m-1) + (n-1)` 回の移動のうち、どの `m-1` 回を「下」への移動にするか(残りは「右」になる)を選択する組み合わせの問題として解釈できる。したがって、組み合わせの公式 `C(m+n-2, m-1)` を用いて解くことができる。

## step1
`math.comb` を利用して、組み合わせ計算を直接実装した。これは最も簡潔で効率的な解法である。

```python
import math


class Solution:
def uniquePaths(self, m: int, n: int) -> int:
if m == 1 and n == 1:
return 1

Choose a reason for hiding this comment

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

if 文なくても動きますか?
https://docs.python.org/3/library/math.html#math.comb
math.comb(0, 0) = 0! / (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.

レビューありがとうございます。
確かに動きますね!

return math.comb(m + n - 2, m - 1)
```

- 時間計算量:`O(min(m, n))`。`math.comb`の実装に依存するが、効率的に計算される。
- 空間計算量:`O(1)`。

## step2
`math.comb` に頼らず、組み合わせを計算する関数を自前で実装した。これにより、ライブラリの内部実装への理解を深め、特定のバージョンに依存しないコードとなる。

```python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def combination(n: int, k: int) -> int:
assert 0 <= k <= n
if n == k == 0:
return 1
if n - k < k:
k = n - k
numerator = 1
denominator = 1

Choose a reason for hiding this comment

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

個人的には処理の直前で変数の用意をした方が好きなので、denominator = 1 は L46 の下に書くのもありかなと思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かに読み手が目でたどる必要がなくなり、読みやすくなりそうですね。アドバイスありがとうございます。

for num in range(n - k + 1, n + 1):
numerator *= num
for num in range(1, k + 1):
denominator *= num
return numerator // denominator

moves_to_right = n - 1
moves_to_bottom = m - 1
total_moves = moves_to_right + moves_to_bottom
return combination(total_moves, moves_to_bottom)

Choose a reason for hiding this comment

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

ここは以下のようにして1行コメントを入れたりするのも選択肢としてよさそうかなと感じました。
return combination(n + m - 2, m - 1)

```

- 時間計算量:`O(min(m, n))`。組み合わせの計算 `C(n, k)` のループ回数は `k` に比例するため。
- 空間計算量:`O(1)`。

# 別解・模範解答

### 1. 動的計画法 (2D DP)
`dp[i][j]` を `(i, j)` に到達する経路の数と定義する。`dp[i][j] = dp[i-1][j] + dp[i][j-1]` という漸化式で解くことができる。

```python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
num_paths = [[0] * n for _ in range(m)]
for row in range(m):
for column in range(n):
if row == 0 or column == 0:
num_paths[row][column] = 1
continue
num_paths[row][column] = num_paths[row - 1][column] + num_paths[row][column - 1]

return num_paths[-1][-1]
```

- 時間計算量:`O(m*n)`。グリッドの全マスを一度ずつ計算するため。
- 空間計算量:`O(m*n)`。`m x n` のDPテーブルを保持するため。

### 2. 動的計画法 (1D DP)
2D DPの空間計算量を最適化したアプローチである。`i`行目の計算は`i-1`行目の情報のみに依存するため、1次元配列を使い回すことで空間を削減できる。

```python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
num_paths_in_row = [1] * n
for _ in range(1, m):
for column in range(1, n):
num_paths_in_row[column] += num_paths_in_row[column - 1]
return num_paths_in_row[-1]
```

- 時間計算量:`O(m*n)`。
- 空間計算量:`O(n)`。1行分の情報を保持する配列のみ使用するため。

### 3. 再帰 (メモ化)
トップダウンの動的計画法アプローチである。`@cache`デコレータ(メモ化)を使い、重複する計算を避ける。

```python
from functools import cache


class Solution:
@cache
Copy link

Choose a reason for hiding this comment

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

デコレーターの振る舞いで、インスタンス間でキャッシュが共有されるか、などは確認しておいてください。
デコレーターは高階関数で、cache は高階関数のローカル変数にキャッシュを持ちます。また、Python はインスタンスごとにメソッドをバインドするので、関数のオブジェクトとして異なれば別のキャッシュになります。
https://github.com/python/cpython/blob/75b1afe562c02962393cbbbf3dce9a8d7be1e19e/Lib/functools.py#L602

class A:
    def f():
        pass

obj1 = A()
obj2 = A()

print(obj1.f is obj2.f)
print(obj1.f)
print(obj2.f)

def uniquePaths(self, m: int, n: int) -> int:
if m == 1 or n == 1:
return 1
return self.uniquePaths(m - 1, n) + self.uniquePaths(m, n - 1)
```

- 時間計算量:`O(m*n)`。メモ化により、各 `(m, n)` の組み合わせは一度しか計算されないため。
- 空間計算量:`O(m*n)`。再帰のスタックとキャッシュのための空間が必要である。

# 次に解く問題
- [Binary Tree Level Order Traversal - LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/)
38 changes: 38 additions & 0 deletions 62_unique_paths_medium/bfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
from collections import deque


class Solution:
def uniquePaths(self, m: int, n: int) -> int:
top_left = (0, 0)
num_paths_grid = [[0 for _ in range(n)] for _ in range(m)]
num_paths_grid[top_left[0]][top_left[1]] = 1
queue: deque[tuple[int, int]] = deque([top_left])
directions = ((0, 1), (1, 0))
visited: set[tuple[int, int]] = set([top_left])

def is_valid(row, column) -> bool:
return 0 <= row < m and 0 <= column < n

while queue:
row, column = queue.popleft()
num_paths = num_paths_grid[row][column]
neighbors = [(row + drow, column + dcol) for drow, dcol in directions]
for neighbor_row, neighbor_col in neighbors:
if not is_valid(neighbor_row, neighbor_col):
continue
num_paths_grid[neighbor_row][neighbor_col] += num_paths
if (neighbor_row, neighbor_col) in visited:
continue
visited.add((neighbor_row, neighbor_col))
queue.append((neighbor_row, neighbor_col))
return num_paths_grid[m - 1][n - 1]

Choose a reason for hiding this comment

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

visited は何を表しているか難しいなと感じました。queue に追加したらvisited に追加していますが、この時点で追加した地点の num_paths_grid の値は確定していないように思います。row + column が増加する順に見ているので問題なく動いていますが、やや不思議な印象を受けました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。
「到達済み」だが「走査完了済み」ではないノードというのはpostorder traversalのようなイメージでしょうか。(cf. https://www.geeksforgeeks.org/dsa/postorder-traversal-of-binary-tree/)
確かにBFSでは訪れたらその時点で距離が確定する場面が多いので、不思議なコードに見えるかもしれません。

Choose a reason for hiding this comment

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

木構造ではないので postorder との関連づけるのは難しそうです。
以下のように書いたら queue 追加時点で値が確定しているでしょうか。

        while queue:
            row, column = queue.popleft()
            num_paths = num_paths_grid[row][column]
            if is_valid(row, column + 1):
                num_paths_grid[row][column + 1] += num_paths
                queue.append((row, column + 1))
            if is_valid(row + 1, column):
                num_paths_grid[row + 1][column] += num_paths
                if column == 0:
                    queue.append((row + 1, column))
        return num_paths_grid[m - 1][n - 1]


# @lc code=end
19 changes: 19 additions & 0 deletions 62_unique_paths_medium/recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
from functools import cache


class Solution:
@cache
def uniquePaths(self, m: int, n: int) -> int:
if m == 1 or n == 1:
return 1
return self.uniquePaths(m - 1, n) + self.uniquePaths(m, n - 1)


# @lc code=end
20 changes: 20 additions & 0 deletions 62_unique_paths_medium/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start


import math


class Solution:
def uniquePaths(self, m: int, n: int) -> int:
if m == 1 and n == 1:
return 1
return math.comb(m + n - 2, m - 1)


# @lc code=end
25 changes: 25 additions & 0 deletions 62_unique_paths_medium/step1_comb_from_scratch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def combination(n: int, k: int) -> int:
assert 0 <= k <= n
if n == k == 0:
return 1
numerator = 1
denominator = 1
for num in range(n - k + 1, n + 1):
numerator *= num
for num in range(1, k + 1):
denominator *= num
return numerator // denominator

return combination(n + m - 2, m - 1)


# @lc code=end
28 changes: 28 additions & 0 deletions 62_unique_paths_medium/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def combination(n: int, k: int) -> int:
assert 0 <= k <= n
if n == k == 0:
return 1
numerator = 1
denominator = 1
for num in range(n - k + 1, n + 1):
numerator *= num
for num in range(1, k + 1):
denominator *= num
return numerator // denominator

vertical_moves = m - 1
horizontal_moves = n - 1
total_moves = vertical_moves + horizontal_moves
return combination(total_moves, vertical_moves)


# @lc code=end
30 changes: 30 additions & 0 deletions 62_unique_paths_medium/step2_comb_from_scratch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def combination(n: int, k: int) -> int:
assert 0 <= k <= n
if n == k == 0:
return 1
if n - k < k:
k = n - k
numerator = 1
denominator = 1
for num in range(n - k + 1, n + 1):
numerator *= num
for num in range(1, k + 1):
denominator *= num
return numerator // denominator

moves_to_right = n - 1
moves_to_bottom = m - 1
total_moves = moves_to_right + moves_to_bottom
return combination(total_moves, moves_to_bottom)


# @lc code=end
18 changes: 18 additions & 0 deletions 62_unique_paths_medium/step3_1dp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
num_paths_in_row = [0] * n
num_paths_in_row[0] = 1
for _ in range(m):
for column in range(1, n):
num_paths_in_row[column] += num_paths_in_row[column - 1]
return num_paths_in_row[-1]


# @lc code=end
19 changes: 19 additions & 0 deletions 62_unique_paths_medium/step3_math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# @lc app=leetcode id=62 lang=python3
#
# [62] Unique Paths
#

# @lc code=start
import math


class Solution:
def uniquePaths(self, m: int, n: int) -> int:
moves_to_bottom = m - 1
moves_to_right = n - 1
total_moves = moves_to_bottom + moves_to_right
return math.comb(total_moves, moves_to_bottom)


# @lc code=end