Unique Paths
ロボットは、m 行 n 列のグリッドの左上のセルからスタートし、右下のセルに到達する必要があります。移動するたびに、右に 1 セル、または下に 1 セル進みます。ロボットが進める異なる経路の数を返してください。
関数
- minteger
- グリッド内の行数
- ninteger
- グリッド内の列数
- 戻り値integer
- 左上のセルから右下のセルまでの異なる経路の数
制約
1 ≤ m, n ≤ 100- 答えは最大でも
2 × 109なので、符号付き32ビット整数に収まります。
例
- 入力
- m = 3n = 4
- 出力
- 10
- 説明
- どの経路も下に2回、右に3回進み、合計で5回移動します。下に進む2回を、合計5回の移動のうちどれにするかで経路が決まり、その選び方は10通りあります。
- 入力
- m = 1n = 6
- 出力
- 1
- 説明
- 1行だけの場合、ロボットは右に5回しか進めないため、経路はちょうど1つです。
- 入力
- m = 4n = 5
- 出力
- 35
- 説明
- 各経路には、下への移動が3回、右への移動が4回あります。7回の移動のうち、どの3回を下への移動にするかを選ぶと、7 × 6 × 5 / 6 = 35通りの経路があります。
提出時に隠しテスト+14件
発展問題
100 × 100 のグリッドでは、答えは59桁になります。i で割る方法が使えなくなった場合、数式を使って 10^9+7 で割った余りをどう返しますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
ロボットがマスに入る直前、どこにいた可能性がありますか?
あるセルに入る経路は、その上のセルに入る経路と、その左のセルに入る経路を合わせたものです。最上段の各セルと最左列の各セルには、それぞれ経路がちょうど1つあります。
個数を行ごとに左から右へ埋めていき、数値は1行だけに保ちます。または、移動の順序を直接数えます。経路とは、
m-1個の下向きの移動を、m+n-2回の移動の中から選ぶことです。
解説
経路を一つずつ列挙するのは無理があります。17 × 17 のグリッドには、すでに 601,080,390 通りの経路があります。列挙せずに数える必要があります。あるセルに至る経路の数は、その上のセルに至る経路の数と、左隣のセルに至る経路の数の合計です。この方法なら、グリッドを一度の処理で埋めていく表にできます。また、経路は下方向と右方向への移動の並びにほかならず、これによって閉じた公式が得られます。
再帰を使ってすべての経路を数える
正しいが、最大のテストでは終わらない
考え方
ロボットが右下のセルに入る最後の移動について考えてみましょう。ロボットは、上のセルから下に移動するか、左のセルから右に移動するかのどちらかで、両方ではありません。したがって、m × n のグリッドを通る経路は、1行少ないグリッドを通る経路 uniquePaths(m-1, n) と、1列少ないグリッドを通る経路 uniquePaths(m, n-1) の合計です。
グリッドが1行または1列になると再帰は終了します。この場合、ロボットはまっすぐ進むことしかできないため、経路はちょうど1つです。すべての経路は2つの移動のうちどちらか一方で終わるので、それぞれの経路は1回だけ数えられ、合計は正しくなります。
各経路は、1を返す基本ケースに到達するため、呼び出し回数は少なくとも答えの数だけになります。そのため処理が遅くなります。17 × 17 のグリッドでは6億回を超える呼び出しが必要で、テストでは答えが約1.6 × 10^9に近い値まで扱われます。同じ小さなグリッドが何度も計算されます。(m-1, n-1) には2つの親それぞれから1回ずつ到達し、下に進むにつれて重複が増えていきます。
アルゴリズム
mまたはnが 1 の場合は 1 を返します。経路は直線の 1 つだけです。- 最後の移動が下方向である経路の数を数えます。
uniquePaths(m-1, n) - 最後の移動が右方向である経路の数を数えます。
uniquePaths(m, n-1) - それらの合計を返します。
def uniquePaths(m, n):
# One row or one column: the only path is a straight line
if m == 1 or n == 1:
return 1
# The last move came down from the row above or right from the column before
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1)グリッドを1行ずつ埋める
考え方
再帰では同じセルを何度も調べますが、セルは全部で m × n 個しかありません。必要なセルが常に計算済みになっている順序で、各セルに至る経路数を一度だけ数えましょう。
状態: paths[r][c] は、左上のセルから行 r、列 c までの経路数です。 漸化式: paths[r][c] = paths[r-1][c] + paths[r][c-1]。上から来る経路と、左から来る経路を足します。 基底ケース: 最上行と最左列の各セルには、一直線の経路が1つあります。 順序: 行ごとに左から右へ進みます。こうすれば、必要になる前に上のセルと左のセルが計算済みになります。
m = 3、n = 4 の場合、各行は 1 1 1 1、次に 1 2 3 4、最後に 1 3 6 10 となり、答えは最後のセルの10です。
次に、計算時に参照するものを見てみましょう。参照するのは、直上の行と計算中の行だけです。そこで、1行だけ保持します。 row[c] を更新する前は、まだ直上の行の経路数が入っています。また、row[c-1] には左隣の新しい経路数がすでに入っているので、row[c] += row[c-1] だけで漸化式をそのまま計算できます。時間計算量は O(m × n) のままで、メモリ使用量は O(m × n) から O(n) に減ります。
アルゴリズム
n個の要素がすべて 1 のrowを作ります。これが最上段の行です。- 最上段より下の各行について、
m-1回繰り返します。 - 各行で、
cを 1 からn-1まで変化させ、row[c-1]をrow[c]に加えます。row[0]は 1 のままです。これが左端の列です。 row[n-1]を返します。
def uniquePaths(m, n):
# row[c] counts the paths into column c of the current row.
# The top row is all 1s: the only way along it is straight right.
row = [1] * n
for _ in range(m - 1):
for c in range(1, n):
# Paths from above (the old row[c]) plus paths from the left (the new row[c-1])
row[c] += row[c - 1]
return row[n - 1]二項係数で移動回数を数える
考え方
すべての経路は、下に進む移動をちょうど m-1 回、右に進む移動をちょうど n-1 回、合計で m+n-2 回、何らかの順序で行います。どの順序も有効な経路です。ロボットが下に m-1 回より多く進んだり、右に n-1 回より多く進んだりすることはないため、グリッドから出ることはありません。したがって、経路は m+n-2 回の移動のうち、どの m-1 回を下に進む移動にするかを選ぶことと同じで、答えは二項係数 C(m+n-2, m-1) です。
前の方法で使った表は、パスカルの三角形を横向きにしたものなので、2つの方法で同じ結果になります。巨大な階乗を使わずに係数を計算するには、因子を1つずつ掛けていきます。N = m+n-2、k = min(m, n)-1 として、i が1から k までの各ステップで、N-k+i を掛けてから i で割ります。ステップ i の後の累積値は C(N-k+i, i) という整数になるため、毎回割り切れます。
m = 3、n = 4 の場合、N = 5、k = 2 となり、値は 1 × 4 / 1 = 4、続いて 4 × 5 / 2 = 10 と変化します。短い方の辺に沿って選ぶことで、ループは最大99ステップになります。最後の除算の前の積は、答えの k 倍です。17 × 17 のグリッドでは、16 × 601,080,390、つまり約 9.6 × 10^9 となり、32ビットの範囲を超えるため、64ビット整数に格納してください。
アルゴリズム
- 移動回数を表す
N = m+n-2を設定し、k = min(m, n)-1とします。 - 64ビットのカウントを1で開始します。
iを1からkまで変化させ、カウントにN-k+iを掛けてから、iで割ります。- カウントを返します。
def uniquePaths(m, n):
# A path is m+n-2 moves; count the ways to choose which of them go down.
# Choose along the shorter side so the loop stays short.
moves = m + n - 2
k = min(m, n) - 1
count = 1
for i in range(1, k + 1):
# count goes from C(moves-k+i-1, i-1) to C(moves-k+i, i); the division is exact
count = count * (moves - k + i) // i
return count
落とし穴と境界ケース
数え上げ自体は短いため、バグはグリッドの端や数値の大きさに潜んでいます。
(m+n-2)!を計算して、ほかの2つの階乗で割る方法では、答えがオーバーフローするよりずっと前にオーバーフローします。21! はすでに64ビットの範囲を超えており、100 × 7 のグリッドではm+n-2は105になります。count / i * (N-k+i)のように乗算の前に割ると、切り捨てが発生します。countが常にiの倍数とは限らないためです。先に乗算しましょう。積は常に割り切れます。count × (N-k+i)の積は、答えが2^31を超えない場合でも、2^31を超えることがあります。64ビット整数に格納してください。- 一番上の行または一番左の列を1ではなく0のままにすると、すべてのセルが0になります。行が1つ、または列が1つのグリッドには、経路がちょうど1つあります。
- 行と列を入れ替えても、答えは変わりません。
C(m+n-2, m-1) = C(m+n-2, n-1)だからです。
よくある質問4
Unique Paths の公式は何ですか?
答えは二項係数 C(m+n-2, m-1) です。どの経路も、m-1 回下に、n-1 回右に、順不同で移動します。また、m+n-2 回の移動のうち、どれを下に進む移動にするかを選べば、経路が決まります。3 × 4 のグリッドでは、C(5, 2) = 10 です。
Unique Paths の時間計算量はどれくらいですか?
動的計画法のテーブルは、O(m × n)の時間がかかり、1行を保持する場合はO(n)の領域を使います。二項係数の公式は、O(min(m, n))の時間とO(1)の領域で計算できます。単純な再帰では、経路の数以上の呼び出しが発生し、その数はm + nに対して指数関数的に増加します。
一部のセルがブロックされている場合、Unique Paths はどのように解きますか?
同じ表を使い、通行できないセルの数を 0 に設定すると、そこを通る経路はなくなります。最上段と左端の列はすべて 1 ではなくなり、最上段では通行できないセルより先にあるすべてのセルの経路数が 0 になります。この式は、移動のあらゆる順序が許可されていることを前提としているため、もう使えません。
なぜ Unique Paths の表はパスカルの三角形と一致するのでしょうか?
各セルは、その上のセルと左のセルを足します。これが、対角線に沿って読むとパスカルの三角形を作る規則です。行 r、列 c のセルには C(r+c, r) が入り、右下のセルには C(m+n-2, m-1) が入ります。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def uniquePaths(m, n):
# ここにコードを書いてくださいケース1
ケース2
ケース3
入力
m = 3 n = 4
期待値
10