Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

Unique Paths - LeetCode

言語

Python

問題の概要

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

自分の解法

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

step1

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

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 に頼らず、組み合わせを計算する関数を自前で実装した。これにより、ライブラリの内部実装への理解を深め、特定のバージョンに依存しないコードとなる。

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] という漸化式で解くことができる。

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次元配列を使い回すことで空間を削減できる。

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デコレータ(メモ化)を使い、重複する計算を避ける。

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)。再帰のスタックとキャッシュのための空間が必要である。

次に解く問題

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.

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

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]

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.

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

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)



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)

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.

5 participants