diff --git a/62_unique_paths_medium/1dp.py b/62_unique_paths_medium/1dp.py new file mode 100644 index 0000000..03f549c --- /dev/null +++ b/62_unique_paths_medium/1dp.py @@ -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 diff --git a/62_unique_paths_medium/2dp.py b/62_unique_paths_medium/2dp.py new file mode 100644 index 0000000..d32b1d0 --- /dev/null +++ b/62_unique_paths_medium/2dp.py @@ -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 diff --git a/62_unique_paths_medium/README.md b/62_unique_paths_medium/README.md new file mode 100644 index 0000000..fb0a16f --- /dev/null +++ b/62_unique_paths_medium/README.md @@ -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 + 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 + 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) +``` + +- 時間計算量:`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 + 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/) diff --git a/62_unique_paths_medium/bfs.py b/62_unique_paths_medium/bfs.py new file mode 100644 index 0000000..3620cfc --- /dev/null +++ b/62_unique_paths_medium/bfs.py @@ -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] + + +# @lc code=end diff --git a/62_unique_paths_medium/recursive.py b/62_unique_paths_medium/recursive.py new file mode 100644 index 0000000..798266a --- /dev/null +++ b/62_unique_paths_medium/recursive.py @@ -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 diff --git a/62_unique_paths_medium/step1.py b/62_unique_paths_medium/step1.py new file mode 100644 index 0000000..3154ad8 --- /dev/null +++ b/62_unique_paths_medium/step1.py @@ -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 diff --git a/62_unique_paths_medium/step1_comb_from_scratch.py b/62_unique_paths_medium/step1_comb_from_scratch.py new file mode 100644 index 0000000..3f668e2 --- /dev/null +++ b/62_unique_paths_medium/step1_comb_from_scratch.py @@ -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 diff --git a/62_unique_paths_medium/step2.py b/62_unique_paths_medium/step2.py new file mode 100644 index 0000000..cfa84fd --- /dev/null +++ b/62_unique_paths_medium/step2.py @@ -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 diff --git a/62_unique_paths_medium/step2_comb_from_scratch.py b/62_unique_paths_medium/step2_comb_from_scratch.py new file mode 100644 index 0000000..9cc4cda --- /dev/null +++ b/62_unique_paths_medium/step2_comb_from_scratch.py @@ -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 diff --git a/62_unique_paths_medium/step3_1dp.py b/62_unique_paths_medium/step3_1dp.py new file mode 100644 index 0000000..03f549c --- /dev/null +++ b/62_unique_paths_medium/step3_1dp.py @@ -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 diff --git a/62_unique_paths_medium/step3_math.py b/62_unique_paths_medium/step3_math.py new file mode 100644 index 0000000..9281d24 --- /dev/null +++ b/62_unique_paths_medium/step3_math.py @@ -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